forked from TrueCloudLab/lego
Merge branch 'master' into acmev2
# Conflicts: # providers/dns/azure/azure.go
This commit is contained in:
commit
ef5b5bffb6
24 changed files with 1760 additions and 13 deletions
6
cli.go
6
cli.go
|
@ -199,18 +199,23 @@ Here is an example bash command using the CloudFlare DNS provider:
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
fmt.Fprintln(w, "\tazure:\tAZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP")
|
fmt.Fprintln(w, "\tazure:\tAZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP")
|
||||||
fmt.Fprintln(w, "\tauroradns:\tAURORA_USER_ID, AURORA_KEY, AURORA_ENDPOINT")
|
fmt.Fprintln(w, "\tauroradns:\tAURORA_USER_ID, AURORA_KEY, AURORA_ENDPOINT")
|
||||||
|
fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW")
|
||||||
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY")
|
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY")
|
||||||
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
|
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
|
||||||
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
|
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
|
||||||
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN")
|
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN")
|
||||||
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET")
|
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET")
|
||||||
|
fmt.Fprintln(w, "\tduckdns:\tDUCKDNS_TOKEN")
|
||||||
fmt.Fprintln(w, "\texoscale:\tEXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT")
|
fmt.Fprintln(w, "\texoscale:\tEXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT")
|
||||||
fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
|
fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY")
|
||||||
fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY")
|
fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY")
|
||||||
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE")
|
fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE")
|
||||||
|
fmt.Fprintln(w, "\tglesys:\tGLESYS_API_USER, GLESYS_API_KEY")
|
||||||
fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY")
|
fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY")
|
||||||
|
fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE")
|
||||||
fmt.Fprintln(w, "\tmanual:\tnone")
|
fmt.Fprintln(w, "\tmanual:\tnone")
|
||||||
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
|
fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY")
|
||||||
|
fmt.Fprintln(w, "\tnamedotcom:\tNAMECOM_USERNAME, NAMECOM_API_TOKEN")
|
||||||
fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY")
|
fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY")
|
||||||
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
|
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
|
||||||
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID")
|
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID")
|
||||||
|
@ -220,6 +225,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
||||||
fmt.Fprintln(w, "\tpdns:\tPDNS_API_KEY, PDNS_API_URL")
|
fmt.Fprintln(w, "\tpdns:\tPDNS_API_KEY, PDNS_API_URL")
|
||||||
fmt.Fprintln(w, "\tdnspod:\tDNSPOD_API_KEY")
|
fmt.Fprintln(w, "\tdnspod:\tDNSPOD_API_KEY")
|
||||||
fmt.Fprintln(w, "\totc:\tOTC_USER_NAME, OTC_PASSWORD, OTC_PROJECT_NAME, OTC_DOMAIN_NAME, OTC_IDENTITY_ENDPOINT")
|
fmt.Fprintln(w, "\totc:\tOTC_USER_NAME, OTC_PASSWORD, OTC_PROJECT_NAME, OTC_DOMAIN_NAME, OTC_IDENTITY_ENDPOINT")
|
||||||
|
fmt.Fprintln(w, "\texec:\tEXEC_PATH")
|
||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
fmt.Println(`
|
fmt.Println(`
|
||||||
|
|
|
@ -27,7 +27,6 @@ type DNSProvider struct {
|
||||||
subscriptionId string
|
subscriptionId string
|
||||||
tenantId string
|
tenantId string
|
||||||
resourceGroup string
|
resourceGroup string
|
||||||
|
|
||||||
context context.Context
|
context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +46,13 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
// DNSProvider instance configured for azure.
|
// DNSProvider instance configured for azure.
|
||||||
func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) {
|
func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) {
|
||||||
if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" {
|
if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" {
|
||||||
return nil, fmt.Errorf("Azure configuration missing")
|
missingEnvVars := []string{}
|
||||||
|
for _, envVar := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP"} {
|
||||||
|
if os.Getenv(envVar) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Azure configuration missing: %s", strings.Join(missingEnvVars, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
|
|
|
@ -58,7 +58,7 @@ func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
os.Setenv("AZURE_SUBSCRIPTION_ID", "")
|
os.Setenv("AZURE_SUBSCRIPTION_ID", "")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.EqualError(t, err, "Azure configuration missing")
|
assert.EqualError(t, err, "Azure configuration missing: AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,AZURE_RESOURCE_GROUP")
|
||||||
restoreAzureEnv()
|
restoreAzureEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
418
providers/dns/bluecat/bluecat.go
Normal file
418
providers/dns/bluecat/bluecat.go
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
// Package bluecat implements a DNS provider for solving the DNS-01 challenge
|
||||||
|
// using a self-hosted Bluecat Address Manager.
|
||||||
|
package bluecat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bluecatUrlTemplate = "%s/Services/REST/v1"
|
||||||
|
const configType = "Configuration"
|
||||||
|
const viewType = "View"
|
||||||
|
const txtType = "TXTRecord"
|
||||||
|
const zoneType = "Zone"
|
||||||
|
|
||||||
|
type entityResponse struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Properties string `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
|
||||||
|
// Bluecat's Address Manager REST API to manage TXT records for a domain.
|
||||||
|
type DNSProvider struct {
|
||||||
|
baseUrl string
|
||||||
|
userName string
|
||||||
|
password string
|
||||||
|
configName string
|
||||||
|
dnsView string
|
||||||
|
token string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.
|
||||||
|
// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL,
|
||||||
|
// BLUECAT_USER_NAME and BLUECAT_PASSWORD. BLUECAT_SERVER_URL should have the
|
||||||
|
// scheme, hostname, and port (if required) of the authoritative Bluecat BAM
|
||||||
|
// server. The REST endpoint will be appended. In addition, the Configuration name
|
||||||
|
// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and
|
||||||
|
// BLUECAT_DNS_VIEW
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
server := os.Getenv("BLUECAT_SERVER_URL")
|
||||||
|
userName := os.Getenv("BLUECAT_USER_NAME")
|
||||||
|
password := os.Getenv("BLUECAT_PASSWORD")
|
||||||
|
configName := os.Getenv("BLUECAT_CONFIG_NAME")
|
||||||
|
dnsView := os.Getenv("BLUECAT_DNS_VIEW")
|
||||||
|
httpClient := http.Client{Timeout: time.Duration(30 * time.Second)}
|
||||||
|
return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
|
// DNSProvider instance configured for Bluecat DNS.
|
||||||
|
func NewDNSProviderCredentials(server, userName, password, configName, dnsView string, httpClient http.Client) (*DNSProvider, error) {
|
||||||
|
if server == "" || userName == "" || password == "" || configName == "" || dnsView == "" {
|
||||||
|
return nil, fmt.Errorf("Bluecat credentials missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
baseUrl: fmt.Sprintf(bluecatUrlTemplate, server),
|
||||||
|
userName: userName,
|
||||||
|
password: password,
|
||||||
|
configName: configName,
|
||||||
|
dnsView: dnsView,
|
||||||
|
httpClient: http.DefaultClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a REST request, using query parameters specified. The Authorization
|
||||||
|
// header will be set if we have an active auth token
|
||||||
|
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
|
||||||
|
url := fmt.Sprintf("%s/%s", d.baseUrl, resource)
|
||||||
|
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if len(d.token) > 0 {
|
||||||
|
req.Header.Set("Authorization", d.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all query parameters
|
||||||
|
q := req.URL.Query()
|
||||||
|
for argName, argVal := range queryArgs {
|
||||||
|
q.Add(argName, argVal)
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
resp, err := d.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
errBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
errResp := string(errBytes)
|
||||||
|
return nil, fmt.Errorf("Bluecat API request failed with HTTP status code %d\n Full message: %s",
|
||||||
|
resp.StatusCode, errResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts a new Bluecat API Session. Authenticates using customerName, userName,
|
||||||
|
// password and receives a token to be used in for subsequent requests.
|
||||||
|
func (d *DNSProvider) login() error {
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"username": d.userName,
|
||||||
|
"password": d.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "login", nil, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
authBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
authResp := string(authBytes)
|
||||||
|
|
||||||
|
if strings.Contains(authResp, "Authentication Error") {
|
||||||
|
msg := strings.Trim(authResp, "\"")
|
||||||
|
return fmt.Errorf("Bluecat API request failed: %s", msg)
|
||||||
|
}
|
||||||
|
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
|
||||||
|
re := regexp.MustCompile("BAMAuthToken: [^ ]+")
|
||||||
|
token := re.FindString(authResp)
|
||||||
|
d.token = token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys Bluecat Session
|
||||||
|
func (d *DNSProvider) logout() error {
|
||||||
|
if len(d.token) == 0 {
|
||||||
|
// nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "logout", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode)
|
||||||
|
} else {
|
||||||
|
authBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
authResp := string(authBytes)
|
||||||
|
|
||||||
|
if !strings.Contains(authResp, "successfully") {
|
||||||
|
msg := strings.Trim(authResp, "\"")
|
||||||
|
return fmt.Errorf("Bluecat API request failed to delete session: %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.token = ""
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup the entity ID of the configuration named in our properties
|
||||||
|
func (d *DNSProvider) lookupConfId() (uint, error) {
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"parentId": strconv.Itoa(0),
|
||||||
|
"name": d.configName,
|
||||||
|
"type": configType,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var conf entityResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&conf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return conf.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the DNS view with the given name within
|
||||||
|
func (d *DNSProvider) lookupViewId(viewName string) (uint, error) {
|
||||||
|
confId, err := d.lookupConfId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"parentId": strconv.FormatUint(uint64(confId), 10),
|
||||||
|
"name": d.dnsView,
|
||||||
|
"type": viewType,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var view entityResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&view)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the entityId of the parent zone by recursing from the root view
|
||||||
|
// Also return the simple name of the host
|
||||||
|
func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string, error) {
|
||||||
|
parentViewId := viewId
|
||||||
|
name := ""
|
||||||
|
|
||||||
|
if fqdn != "" {
|
||||||
|
zones := strings.Split(strings.Trim(fqdn, "."), ".")
|
||||||
|
last := len(zones) - 1
|
||||||
|
name = zones[0]
|
||||||
|
|
||||||
|
for i := last; i > -1; i-- {
|
||||||
|
zoneId, err := d.getZone(parentViewId, zones[i])
|
||||||
|
if err != nil || zoneId == 0 {
|
||||||
|
return parentViewId, name, err
|
||||||
|
}
|
||||||
|
if (i > 0) {
|
||||||
|
name = strings.Join(zones[0:i],".")
|
||||||
|
}
|
||||||
|
parentViewId = zoneId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentViewId, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the DNS zone with the specified name under the parentId
|
||||||
|
func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) {
|
||||||
|
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"parentId": strconv.FormatUint(uint64(parentId), 10),
|
||||||
|
"name": name,
|
||||||
|
"type": zoneType,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
|
||||||
|
// Return an empty zone if the named zone doesn't exist
|
||||||
|
if resp != nil && resp.StatusCode == 404 {
|
||||||
|
return 0, fmt.Errorf("Bluecat API could not find zone named %s", name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var zone entityResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&zone)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return zone.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record using the specified parameters
|
||||||
|
// This will *not* create a subzone to contain the TXT record,
|
||||||
|
// so make sure the FQDN specified is within an extant zone.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
err := d.login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewId, err := d.lookupViewId(d.dnsView)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentZoneId, name, err := d.lookupParentZoneId(viewId, fqdn)
|
||||||
|
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"parentId": strconv.FormatUint(uint64(parentZoneId), 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
body := bluecatEntity{
|
||||||
|
Name: name,
|
||||||
|
Type: "TXTRecord",
|
||||||
|
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("POST", "addEntity", body, queryArgs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
addTxtBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
addTxtResp := string(addTxtBytes)
|
||||||
|
// addEntity responds only with body text containing the ID of the created record
|
||||||
|
_, err = strconv.ParseUint(addTxtResp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.deploy(uint(parentZoneId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.logout()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy the DNS config for the specified entity to the authoritative servers
|
||||||
|
func (d *DNSProvider) deploy(entityId uint) error {
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"entityId": strconv.FormatUint(uint64(entityId), 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
err := d.login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewId, err := d.lookupViewId(d.dnsView)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentId, name, err := d.lookupParentZoneId(viewId, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
queryArgs := map[string]string{
|
||||||
|
"parentId": strconv.FormatUint(uint64(parentId), 10),
|
||||||
|
"name": name,
|
||||||
|
"type": txtType,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var txtRec entityResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&txtRec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
queryArgs = map[string]string{
|
||||||
|
"objectId": strconv.FormatUint(uint64(txtRec.Id), 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
err = d.deploy(parentId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.logout()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//JSON body for Bluecat entity requests and responses
|
||||||
|
type bluecatEntity struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Properties string `json:"properties"`
|
||||||
|
}
|
57
providers/dns/bluecat/bluecat_test.go
Normal file
57
providers/dns/bluecat/bluecat_test.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package bluecat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bluecatLiveTest bool
|
||||||
|
bluecatServer string
|
||||||
|
bluecatUserName string
|
||||||
|
bluecatPassword string
|
||||||
|
bluecatConfigName string
|
||||||
|
bluecatDnsView string
|
||||||
|
bluecatDomain string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
bluecatServer = os.Getenv("BLUECAT_SERVER_URL")
|
||||||
|
bluecatUserName = os.Getenv("BLUECAT_USER_NAME")
|
||||||
|
bluecatPassword = os.Getenv("BLUECAT_PASSWORD")
|
||||||
|
bluecatDomain = os.Getenv("BLUECAT_DOMAIN")
|
||||||
|
bluecatConfigName = os.Getenv("BLUECAT_CONFIG_NAME")
|
||||||
|
bluecatDnsView = os.Getenv("BLUECAT_DNS_VIEW")
|
||||||
|
if len(bluecatServer) > 0 && len(bluecatDomain) > 0 && len(bluecatUserName) > 0 && len(bluecatPassword) > 0 && len(bluecatConfigName) > 0 && len(bluecatDnsView) > 0 {
|
||||||
|
bluecatLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveBluecatPresent(t *testing.T) {
|
||||||
|
if !bluecatLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(bluecatDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveBluecatCleanUp(t *testing.T) {
|
||||||
|
if !bluecatLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(bluecatDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
@ -37,7 +38,14 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
// DNSProvider instance configured for cloudflare.
|
// DNSProvider instance configured for cloudflare.
|
||||||
func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
|
func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
|
||||||
if email == "" || key == "" {
|
if email == "" || key == "" {
|
||||||
return nil, fmt.Errorf("CloudFlare credentials missing")
|
missingEnvVars := []string{}
|
||||||
|
if email == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, "CLOUDFLARE_EMAIL")
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, "CLOUDFLARE_API_KEY")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("CloudFlare credentials missing: %s", strings.Join(missingEnvVars, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
|
|
|
@ -49,7 +49,14 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
os.Setenv("CLOUDFLARE_EMAIL", "")
|
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||||
os.Setenv("CLOUDFLARE_API_KEY", "")
|
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.EqualError(t, err, "CloudFlare credentials missing")
|
assert.EqualError(t, err, "CloudFlare credentials missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY")
|
||||||
|
restoreCloudFlareEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderMissingCredErrSingle(t *testing.T){
|
||||||
|
os.Setenv("CLOUDFLARE_EMAIL", "awesome@possum.com")
|
||||||
|
_, err:= NewDNSProvider()
|
||||||
|
assert.EqualError(t, err, "CloudFlare credentials missing: CLOUDFLARE_API_KEY")
|
||||||
restoreCloudFlareEnv()
|
restoreCloudFlareEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,20 @@ import (
|
||||||
"github.com/xenolf/lego/providers/dns/dnsimple"
|
"github.com/xenolf/lego/providers/dns/dnsimple"
|
||||||
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
|
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
|
||||||
"github.com/xenolf/lego/providers/dns/dnspod"
|
"github.com/xenolf/lego/providers/dns/dnspod"
|
||||||
|
"github.com/xenolf/lego/providers/dns/duckdns"
|
||||||
"github.com/xenolf/lego/providers/dns/dyn"
|
"github.com/xenolf/lego/providers/dns/dyn"
|
||||||
|
"github.com/xenolf/lego/providers/dns/exec"
|
||||||
"github.com/xenolf/lego/providers/dns/exoscale"
|
"github.com/xenolf/lego/providers/dns/exoscale"
|
||||||
|
"github.com/xenolf/lego/providers/dns/fastdns"
|
||||||
"github.com/xenolf/lego/providers/dns/gandi"
|
"github.com/xenolf/lego/providers/dns/gandi"
|
||||||
"github.com/xenolf/lego/providers/dns/gandiv5"
|
"github.com/xenolf/lego/providers/dns/gandiv5"
|
||||||
"github.com/xenolf/lego/providers/dns/googlecloud"
|
"github.com/xenolf/lego/providers/dns/glesys"
|
||||||
"github.com/xenolf/lego/providers/dns/godaddy"
|
"github.com/xenolf/lego/providers/dns/godaddy"
|
||||||
|
"github.com/xenolf/lego/providers/dns/googlecloud"
|
||||||
|
"github.com/xenolf/lego/providers/dns/lightsail"
|
||||||
"github.com/xenolf/lego/providers/dns/linode"
|
"github.com/xenolf/lego/providers/dns/linode"
|
||||||
"github.com/xenolf/lego/providers/dns/namecheap"
|
"github.com/xenolf/lego/providers/dns/namecheap"
|
||||||
|
"github.com/xenolf/lego/providers/dns/namedotcom"
|
||||||
"github.com/xenolf/lego/providers/dns/ns1"
|
"github.com/xenolf/lego/providers/dns/ns1"
|
||||||
"github.com/xenolf/lego/providers/dns/otc"
|
"github.com/xenolf/lego/providers/dns/otc"
|
||||||
"github.com/xenolf/lego/providers/dns/ovh"
|
"github.com/xenolf/lego/providers/dns/ovh"
|
||||||
|
@ -29,6 +35,7 @@ import (
|
||||||
"github.com/xenolf/lego/providers/dns/rfc2136"
|
"github.com/xenolf/lego/providers/dns/rfc2136"
|
||||||
"github.com/xenolf/lego/providers/dns/route53"
|
"github.com/xenolf/lego/providers/dns/route53"
|
||||||
"github.com/xenolf/lego/providers/dns/vultr"
|
"github.com/xenolf/lego/providers/dns/vultr"
|
||||||
|
"github.com/xenolf/lego/providers/dns/bluecat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
|
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
|
||||||
|
@ -39,6 +46,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
||||||
provider, err = azure.NewDNSProvider()
|
provider, err = azure.NewDNSProvider()
|
||||||
case "auroradns":
|
case "auroradns":
|
||||||
provider, err = auroradns.NewDNSProvider()
|
provider, err = auroradns.NewDNSProvider()
|
||||||
|
case "bluecat":
|
||||||
|
provider, err = bluecat.NewDNSProvider()
|
||||||
case "cloudflare":
|
case "cloudflare":
|
||||||
provider, err = cloudflare.NewDNSProvider()
|
provider, err = cloudflare.NewDNSProvider()
|
||||||
case "cloudxns":
|
case "cloudxns":
|
||||||
|
@ -51,24 +60,34 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
||||||
provider, err = dnsmadeeasy.NewDNSProvider()
|
provider, err = dnsmadeeasy.NewDNSProvider()
|
||||||
case "dnspod":
|
case "dnspod":
|
||||||
provider, err = dnspod.NewDNSProvider()
|
provider, err = dnspod.NewDNSProvider()
|
||||||
|
case "duckdns":
|
||||||
|
provider, err = duckdns.NewDNSProvider()
|
||||||
case "dyn":
|
case "dyn":
|
||||||
provider, err = dyn.NewDNSProvider()
|
provider, err = dyn.NewDNSProvider()
|
||||||
|
case "fastdns":
|
||||||
|
provider, err = fastdns.NewDNSProvider()
|
||||||
case "exoscale":
|
case "exoscale":
|
||||||
provider, err = exoscale.NewDNSProvider()
|
provider, err = exoscale.NewDNSProvider()
|
||||||
case "gandi":
|
case "gandi":
|
||||||
provider, err = gandi.NewDNSProvider()
|
provider, err = gandi.NewDNSProvider()
|
||||||
case "gandiv5":
|
case "gandiv5":
|
||||||
provider, err = gandiv5.NewDNSProvider()
|
provider, err = gandiv5.NewDNSProvider()
|
||||||
|
case "glesys":
|
||||||
|
provider, err = glesys.NewDNSProvider()
|
||||||
case "gcloud":
|
case "gcloud":
|
||||||
provider, err = googlecloud.NewDNSProvider()
|
provider, err = googlecloud.NewDNSProvider()
|
||||||
case "godaddy":
|
case "godaddy":
|
||||||
provider, err = godaddy.NewDNSProvider()
|
provider, err = godaddy.NewDNSProvider()
|
||||||
|
case "lightsail":
|
||||||
|
provider, err = lightsail.NewDNSProvider()
|
||||||
case "linode":
|
case "linode":
|
||||||
provider, err = linode.NewDNSProvider()
|
provider, err = linode.NewDNSProvider()
|
||||||
case "manual":
|
case "manual":
|
||||||
provider, err = acme.NewDNSProviderManual()
|
provider, err = acme.NewDNSProviderManual()
|
||||||
case "namecheap":
|
case "namecheap":
|
||||||
provider, err = namecheap.NewDNSProvider()
|
provider, err = namecheap.NewDNSProvider()
|
||||||
|
case "namedotcom":
|
||||||
|
provider, err = namedotcom.NewDNSProvider()
|
||||||
case "rackspace":
|
case "rackspace":
|
||||||
provider, err = rackspace.NewDNSProvider()
|
provider, err = rackspace.NewDNSProvider()
|
||||||
case "route53":
|
case "route53":
|
||||||
|
@ -85,6 +104,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
||||||
provider, err = ns1.NewDNSProvider()
|
provider, err = ns1.NewDNSProvider()
|
||||||
case "otc":
|
case "otc":
|
||||||
provider, err = otc.NewDNSProvider()
|
provider, err = otc.NewDNSProvider()
|
||||||
|
case "exec":
|
||||||
|
provider, err = exec.NewDNSProvider()
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("Unrecognised DNS provider: %s", name)
|
err = fmt.Errorf("Unrecognised DNS provider: %s", name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,5 +176,5 @@ func (c *DNSProvider) getAccountID() (string, error) {
|
||||||
return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token.")
|
return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.Itoa(whoamiResponse.Data.Account.ID), nil
|
return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil
|
||||||
}
|
}
|
||||||
|
|
82
providers/dns/duckdns/duckdns.go
Normal file
82
providers/dns/duckdns/duckdns.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Adds lego support for http://duckdns.org .
|
||||||
|
//
|
||||||
|
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
|
||||||
|
package duckdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProvider adds and removes the record for the DNS challenge
|
||||||
|
type DNSProvider struct {
|
||||||
|
// The duckdns api token
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a new DNS provider using
|
||||||
|
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
duckdnsToken := os.Getenv("DUCKDNS_TOKEN")
|
||||||
|
|
||||||
|
return NewDNSProviderCredentials(duckdnsToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
|
// DNSProvider instance configured for http://duckdns.org .
|
||||||
|
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
|
||||||
|
if duckdnsToken == "" {
|
||||||
|
return nil, errors.New("environment variable DUCKDNS_TOKEN not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{token: duckdnsToken}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
|
||||||
|
// txt == "" will clear the TXT record.
|
||||||
|
func makeDuckdnsURL(domain, token, txt string) string {
|
||||||
|
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
|
||||||
|
if txt == "" {
|
||||||
|
return requestBase + "&clear=true"
|
||||||
|
}
|
||||||
|
return requestBase + "&txt=" + txt
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueDuckdnsRequest(url string) error {
|
||||||
|
response, err := acme.HTTPClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := string(bodyBytes)
|
||||||
|
if body != "OK" {
|
||||||
|
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||||
|
// In duckdns you only have one TXT record shared with
|
||||||
|
// the domain and all sub domains.
|
||||||
|
//
|
||||||
|
// To update the TXT record we just need to make one simple get request.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
url := makeDuckdnsURL(domain, d.token, txtRecord)
|
||||||
|
return issueDuckdnsRequest(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp clears duckdns TXT record
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
url := makeDuckdnsURL(domain, d.token, "")
|
||||||
|
return issueDuckdnsRequest(url)
|
||||||
|
}
|
65
providers/dns/duckdns/duckdns_test.go
Normal file
65
providers/dns/duckdns/duckdns_test.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package duckdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
duckdnsLiveTest bool
|
||||||
|
duckdnsToken string
|
||||||
|
duckdnsDomain string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
duckdnsToken = os.Getenv("DUCKDNS_TOKEN")
|
||||||
|
duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN")
|
||||||
|
if len(duckdnsDomain) > 0 && len(duckdnsDomain) > 0 {
|
||||||
|
duckdnsLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreDuckdnsEnv() {
|
||||||
|
os.Setenv("DUCKDNS_TOKEN", duckdnsToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||||
|
os.Setenv("DUCKDNS_TOKEN", "123")
|
||||||
|
_, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreDuckdnsEnv()
|
||||||
|
}
|
||||||
|
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
|
os.Setenv("DUCKDNS_TOKEN", "")
|
||||||
|
_, err := NewDNSProvider()
|
||||||
|
assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set")
|
||||||
|
restoreDuckdnsEnv()
|
||||||
|
}
|
||||||
|
func TestLiveDuckdnsPresent(t *testing.T) {
|
||||||
|
if !duckdnsLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(duckdnsDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveDuckdnsCleanUp(t *testing.T) {
|
||||||
|
if !duckdnsLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(duckdnsDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -87,11 +87,8 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 500 {
|
||||||
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode)
|
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode)
|
||||||
} else if resp.StatusCode == 307 {
|
|
||||||
// TODO add support for HTTP 307 response and long running jobs
|
|
||||||
return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var dynRes dynResponse
|
var dynRes dynResponse
|
||||||
|
@ -100,6 +97,13 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages)
|
||||||
|
} else if resp.StatusCode == 307 {
|
||||||
|
// TODO add support for HTTP 307 response and long running jobs
|
||||||
|
return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
if dynRes.Status == "failure" {
|
if dynRes.Status == "failure" {
|
||||||
// TODO add better error handling
|
// TODO add better error handling
|
||||||
return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
|
return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
|
||||||
|
|
72
providers/dns/exec/exec.go
Normal file
72
providers/dns/exec/exec.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// Package exec implements a manual DNS provider which runs a program for
|
||||||
|
// adding/removing the DNS record.
|
||||||
|
//
|
||||||
|
// The file name of the external program is specified in the environment
|
||||||
|
// variable EXEC_PATH. When it is run by lego, three command-line parameters
|
||||||
|
// are passed to it: The action ("present" or "cleanup"), the fully-qualified domain
|
||||||
|
// name, the value for the record and the TTL.
|
||||||
|
//
|
||||||
|
// For example, requesting a certificate for the domain 'foo.example.com' can
|
||||||
|
// be achieved by calling lego as follows:
|
||||||
|
//
|
||||||
|
// EXEC_PATH=./update-dns.sh \
|
||||||
|
// lego --dns exec \
|
||||||
|
// --domains foo.example.com \
|
||||||
|
// --email invalid@example.com run
|
||||||
|
//
|
||||||
|
// It will then call the program './update-dns.sh' with like this:
|
||||||
|
//
|
||||||
|
// ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120"
|
||||||
|
//
|
||||||
|
// The program then needs to make sure the record is inserted. When it returns
|
||||||
|
// an error via a non-zero exit code, lego aborts.
|
||||||
|
//
|
||||||
|
// When the record is to be removed again, the program is called with the first
|
||||||
|
// command-line parameter set to "cleanup" instead of "present".
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProvider adds and removes the record for the DNS challenge by calling a
|
||||||
|
// program with command-line parameters.
|
||||||
|
type DNSProvider struct {
|
||||||
|
program string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a new DNS provider which runs the program in the
|
||||||
|
// environment variable EXEC_PATH for adding and removing the DNS record.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
s := os.Getenv("EXEC_PATH")
|
||||||
|
if s == "" {
|
||||||
|
return nil, errors.New("environment variable EXEC_PATH not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{program: s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
cmd := exec.Command(d.program, "present", fqdn, value, strconv.Itoa(ttl))
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
cmd := exec.Command(d.program, "cleanup", fqdn, value, strconv.Itoa(ttl))
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
139
providers/dns/fastdns/fastdns.go
Normal file
139
providers/dns/fastdns/fastdns.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package fastdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1"
|
||||||
|
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
|
||||||
|
type DNSProvider struct {
|
||||||
|
config edgegrid.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider uses the supplied environment variables to return a DNSProvider instance:
|
||||||
|
// AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET, AKAMAI_ACCESS_TOKEN
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
host := os.Getenv("AKAMAI_HOST")
|
||||||
|
clientToken := os.Getenv("AKAMAI_CLIENT_TOKEN")
|
||||||
|
clientSecret := os.Getenv("AKAMAI_CLIENT_SECRET")
|
||||||
|
accessToken := os.Getenv("AKAMAI_ACCESS_TOKEN")
|
||||||
|
|
||||||
|
return NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderClient uses the supplied parameters to return a DNSProvider instance
|
||||||
|
// configured for FastDNS.
|
||||||
|
func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (*DNSProvider, error) {
|
||||||
|
if clientToken == "" || clientSecret == "" || accessToken == "" || host == "" {
|
||||||
|
return nil, fmt.Errorf("Akamai FastDNS credentials missing")
|
||||||
|
}
|
||||||
|
config := edgegrid.Config{
|
||||||
|
Host: host,
|
||||||
|
ClientToken: clientToken,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
AccessToken: accessToken,
|
||||||
|
MaxBody: 131072,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fullfil the dns-01 challenge.
|
||||||
|
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configdns.Init(c.config)
|
||||||
|
|
||||||
|
zone, err := configdns.GetZone(zoneName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
record := configdns.NewTxtRecord()
|
||||||
|
record.SetField("name", recordName)
|
||||||
|
record.SetField("ttl", ttl)
|
||||||
|
record.SetField("target", value)
|
||||||
|
record.SetField("active", true)
|
||||||
|
|
||||||
|
existingRecord := c.findExistingRecord(zone, recordName)
|
||||||
|
|
||||||
|
if existingRecord != nil {
|
||||||
|
if reflect.DeepEqual(existingRecord.ToMap(), record.ToMap()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
zone.RemoveRecord(existingRecord)
|
||||||
|
return c.createRecord(zone, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.createRecord(zone, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the record matching the specified parameters.
|
||||||
|
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configdns.Init(c.config)
|
||||||
|
|
||||||
|
zone, err := configdns.GetZone(zoneName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingRecord := c.findExistingRecord(zone, recordName)
|
||||||
|
|
||||||
|
if existingRecord != nil {
|
||||||
|
err := zone.RemoveRecord(existingRecord)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return zone.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) {
|
||||||
|
zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
zone = acme.UnFqdn(zone)
|
||||||
|
name := acme.UnFqdn(fqdn)
|
||||||
|
name = name[:len(name)-len("."+zone)]
|
||||||
|
|
||||||
|
return zone, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProvider) findExistingRecord(zone *configdns.Zone, recordName string) *configdns.TxtRecord {
|
||||||
|
for _, r := range zone.Zone.Txt {
|
||||||
|
if r.Name == recordName {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProvider) createRecord(zone *configdns.Zone, record *configdns.TxtRecord) error {
|
||||||
|
err := zone.AddRecord(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return zone.Save()
|
||||||
|
}
|
117
providers/dns/fastdns/fastdns_test.go
Normal file
117
providers/dns/fastdns/fastdns_test.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package fastdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fastdnsLiveTest bool
|
||||||
|
host string
|
||||||
|
clientToken string
|
||||||
|
clientSecret string
|
||||||
|
accessToken string
|
||||||
|
testDomain string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
host = os.Getenv("AKAMAI_HOST")
|
||||||
|
clientToken = os.Getenv("AKAMAI_CLIENT_TOKEN")
|
||||||
|
clientSecret = os.Getenv("AKAMAI_CLIENT_SECRET")
|
||||||
|
accessToken = os.Getenv("AKAMAI_ACCESS_TOKEN")
|
||||||
|
testDomain = os.Getenv("AKAMAI_TEST_DOMAIN")
|
||||||
|
|
||||||
|
if len(host) > 0 && len(clientToken) > 0 && len(clientSecret) > 0 && len(accessToken) > 0 {
|
||||||
|
fastdnsLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreFastdnsEnv() {
|
||||||
|
os.Setenv("AKAMAI_HOST", host)
|
||||||
|
os.Setenv("AKAMAI_CLIENT_TOKEN", clientToken)
|
||||||
|
os.Setenv("AKAMAI_CLIENT_SECRET", clientSecret)
|
||||||
|
os.Setenv("AKAMAI_ACCESS_TOKEN", accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderValid(t *testing.T) {
|
||||||
|
os.Setenv("AKAMAI_HOST", "")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_TOKEN", "")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_SECRET", "")
|
||||||
|
os.Setenv("AKAMAI_ACCESS_TOKEN", "")
|
||||||
|
_, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreFastdnsEnv()
|
||||||
|
}
|
||||||
|
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||||
|
os.Setenv("AKAMAI_HOST", "somehost")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_TOKEN", "someclienttoken")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_SECRET", "someclientsecret")
|
||||||
|
os.Setenv("AKAMAI_ACCESS_TOKEN", "someaccesstoken")
|
||||||
|
_, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreFastdnsEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
|
os.Setenv("AKAMAI_HOST", "")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_TOKEN", "")
|
||||||
|
os.Setenv("AKAMAI_CLIENT_SECRET", "")
|
||||||
|
os.Setenv("AKAMAI_ACCESS_TOKEN", "")
|
||||||
|
|
||||||
|
_, err := NewDNSProvider()
|
||||||
|
assert.EqualError(t, err, "Akamai FastDNS credentials missing")
|
||||||
|
restoreFastdnsEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveFastdnsPresent(t *testing.T) {
|
||||||
|
if !fastdnsLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(testDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Present Twice to handle create / update
|
||||||
|
err = provider.Present(testDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractRootRecordName(t *testing.T) {
|
||||||
|
provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.bar.com.", "bar.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar.com", zone)
|
||||||
|
assert.Equal(t, "_acme-challenge", recordName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractSubRecordName(t *testing.T) {
|
||||||
|
provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.foo.bar.com.", "foo.bar.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bar.com", zone)
|
||||||
|
assert.Equal(t, "_acme-challenge.foo", recordName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveFastdnsCleanUp(t *testing.T) {
|
||||||
|
if !fastdnsLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(testDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
211
providers/dns/glesys/glesys.go
Normal file
211
providers/dns/glesys/glesys.go
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
// Package glesys implements a DNS provider for solving the DNS-01
|
||||||
|
// challenge using GleSYS api.
|
||||||
|
package glesys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation
|
||||||
|
|
||||||
|
// domainAPI is the GleSYS API endpoint used by Present and CleanUp.
|
||||||
|
const domainAPI = "https://api.glesys.com/domain"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Logger is used to log API communication results;
|
||||||
|
// if nil, the default log.Logger is used.
|
||||||
|
Logger *log.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
// logf writes a log entry. It uses Logger if not
|
||||||
|
// nil, otherwise it uses the default log.Logger.
|
||||||
|
func logf(format string, args ...interface{}) {
|
||||||
|
if Logger != nil {
|
||||||
|
Logger.Printf(format, args...)
|
||||||
|
} else {
|
||||||
|
log.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the
|
||||||
|
// acme.ChallengeProviderTimeout interface that uses GleSYS
|
||||||
|
// API to manage TXT records for a domain.
|
||||||
|
type DNSProvider struct {
|
||||||
|
apiUser string
|
||||||
|
apiKey string
|
||||||
|
activeRecords map[string]int
|
||||||
|
inProgressMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for GleSYS.
|
||||||
|
// Credentials must be passed in the environment variables: GLESYS_API_USER
|
||||||
|
// and GLESYS_API_KEY.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
apiUser := os.Getenv("GLESYS_API_USER")
|
||||||
|
apiKey := os.Getenv("GLESYS_API_KEY")
|
||||||
|
return NewDNSProviderCredentials(apiUser, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
|
// DNSProvider instance configured for GleSYS.
|
||||||
|
func NewDNSProviderCredentials(apiUser string, apiKey string) (*DNSProvider, error) {
|
||||||
|
if apiUser == "" || apiKey == "" {
|
||||||
|
return nil, fmt.Errorf("GleSYS DNS: Incomplete credentials provided")
|
||||||
|
}
|
||||||
|
return &DNSProvider{
|
||||||
|
apiUser: apiUser,
|
||||||
|
apiKey: apiKey,
|
||||||
|
activeRecords: make(map[string]int),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record using the specified parameters.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
if ttl < 60 {
|
||||||
|
ttl = 60 // 60 is GleSYS minimum value for ttl
|
||||||
|
}
|
||||||
|
// find authZone
|
||||||
|
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err)
|
||||||
|
}
|
||||||
|
// determine name of TXT record
|
||||||
|
if !strings.HasSuffix(
|
||||||
|
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
||||||
|
}
|
||||||
|
name := fqdn[:len(fqdn)-len("."+authZone)]
|
||||||
|
// acquire lock and check there is not a challenge already in
|
||||||
|
// progress for this value of authZone
|
||||||
|
d.inProgressMu.Lock()
|
||||||
|
defer d.inProgressMu.Unlock()
|
||||||
|
// add TXT record into authZone
|
||||||
|
recordId, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// save data necessary for CleanUp
|
||||||
|
d.activeRecords[fqdn] = recordId
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
// acquire lock and retrieve authZone
|
||||||
|
d.inProgressMu.Lock()
|
||||||
|
defer d.inProgressMu.Unlock()
|
||||||
|
if _, ok := d.activeRecords[fqdn]; !ok {
|
||||||
|
// if there is no cleanup information then just return
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
recordId := d.activeRecords[fqdn]
|
||||||
|
delete(d.activeRecords, fqdn)
|
||||||
|
// delete TXT record from authZone
|
||||||
|
err := d.deleteTXTRecord(domain, recordId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout returns the values (20*time.Minute, 20*time.Second) which
|
||||||
|
// are used by the acme package as timeout and check interval values
|
||||||
|
// when checking for DNS record propagation with GleSYS.
|
||||||
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
|
return 20 * time.Minute, 20 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// types for JSON method calls, parameters, and responses
|
||||||
|
|
||||||
|
type addRecordRequest struct {
|
||||||
|
Domainname string `json:"domainname"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
Ttl int `json:"ttl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteRecordRequest struct {
|
||||||
|
Recordid int `json:"recordid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseStruct struct {
|
||||||
|
Response struct {
|
||||||
|
Status struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
} `json:"status"`
|
||||||
|
Record deleteRecordRequest `json:"record"`
|
||||||
|
} `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// POSTing/Marshalling/Unmarshalling
|
||||||
|
|
||||||
|
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
|
||||||
|
url := fmt.Sprintf("%s/%s", domainAPI, resource)
|
||||||
|
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.SetBasicAuth(d.apiUser, d.apiKey)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
var response responseStruct
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
|
||||||
|
return &response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// functions to perform API actions
|
||||||
|
|
||||||
|
func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) {
|
||||||
|
response, err := d.sendRequest("POST", "addrecord", addRecordRequest{
|
||||||
|
Domainname: domain,
|
||||||
|
Host: name,
|
||||||
|
Type: "TXT",
|
||||||
|
Data: value,
|
||||||
|
Ttl: ttl,
|
||||||
|
})
|
||||||
|
if response != nil && response.Response.Status.Code == 200 {
|
||||||
|
logf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid)
|
||||||
|
return response.Response.Record.Recordid, nil
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error {
|
||||||
|
response, err := d.sendRequest("POST", "deleterecord", deleteRecordRequest{
|
||||||
|
Recordid: recordid,
|
||||||
|
})
|
||||||
|
if response != nil && response.Response.Status.Code == 200 {
|
||||||
|
logf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
60
providers/dns/glesys/glesys_test.go
Normal file
60
providers/dns/glesys/glesys_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package glesys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
glesysAPIUser string
|
||||||
|
glesysAPIKey string
|
||||||
|
glesysDomain string
|
||||||
|
glesysLiveTest bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
glesysAPIUser = os.Getenv("GLESYS_API_USER")
|
||||||
|
glesysAPIKey = os.Getenv("GLESYS_API_KEY")
|
||||||
|
glesysDomain = os.Getenv("GLESYS_DOMAIN")
|
||||||
|
|
||||||
|
if len(glesysAPIUser) > 0 && len(glesysAPIKey) > 0 && len(glesysDomain) > 0 {
|
||||||
|
glesysLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
|
||||||
|
if !glesysLiveTest {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, provider)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSProvider_Present(t *testing.T) {
|
||||||
|
if !glesysLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(glesysDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||||
|
if !glesysLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(glesysDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
107
providers/dns/lightsail/lightsail.go
Normal file
107
providers/dns/lightsail/lightsail.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// Package lightsail implements a DNS provider for solving the DNS-01 challenge
|
||||||
|
// using AWS Lightsail DNS.
|
||||||
|
package lightsail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/client"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/request"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxRetries = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProvider implements the acme.ChallengeProvider interface
|
||||||
|
type DNSProvider struct {
|
||||||
|
client *lightsail.Lightsail
|
||||||
|
}
|
||||||
|
|
||||||
|
// customRetryer implements the client.Retryer interface by composing the
|
||||||
|
// DefaultRetryer. It controls the logic for retrying recoverable request
|
||||||
|
// errors (e.g. when rate limits are exceeded).
|
||||||
|
type customRetryer struct {
|
||||||
|
client.DefaultRetryer
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryRules overwrites the DefaultRetryer's method.
|
||||||
|
// It uses a basic exponential backoff algorithm that returns an initial
|
||||||
|
// delay of ~400ms with an upper limit of ~30 seconds which should prevent
|
||||||
|
// causing a high number of consecutive throttling errors.
|
||||||
|
// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
|
||||||
|
func (d customRetryer) RetryRules(r *request.Request) time.Duration {
|
||||||
|
retryCount := r.RetryCount
|
||||||
|
if retryCount > 7 {
|
||||||
|
retryCount = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
|
||||||
|
return time.Duration(delay) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for the AWS
|
||||||
|
// Lightsail service.
|
||||||
|
//
|
||||||
|
// AWS Credentials are automatically detected in the following locations
|
||||||
|
// and prioritized in the following order:
|
||||||
|
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
|
||||||
|
// [AWS_SESSION_TOKEN], [DNS_ZONE]
|
||||||
|
// 2. Shared credentials file (defaults to ~/.aws/credentials)
|
||||||
|
// 3. Amazon EC2 IAM role
|
||||||
|
//
|
||||||
|
// public hosted zone via the FQDN.
|
||||||
|
//
|
||||||
|
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
r := customRetryer{}
|
||||||
|
r.NumMaxRetries = maxRetries
|
||||||
|
config := request.WithRetryer(aws.NewConfig(), r)
|
||||||
|
client := lightsail.New(session.New(config))
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record using the specified parameters
|
||||||
|
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
value = `"` + value + `"`
|
||||||
|
err := r.newTxtRecord(domain, fqdn, value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
value = `"` + value + `"`
|
||||||
|
params := &lightsail.DeleteDomainEntryInput{
|
||||||
|
DomainName: aws.String(domain),
|
||||||
|
DomainEntry: &lightsail.DomainEntry{
|
||||||
|
Name: aws.String(fqdn),
|
||||||
|
Type: aws.String("TXT"),
|
||||||
|
Target: aws.String(value),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := r.client.DeleteDomainEntry(params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
|
||||||
|
params := &lightsail.CreateDomainEntryInput{
|
||||||
|
DomainName: aws.String(domain),
|
||||||
|
DomainEntry: &lightsail.DomainEntry{
|
||||||
|
Name: aws.String(fqdn),
|
||||||
|
Target: aws.String(value),
|
||||||
|
Type: aws.String("TXT"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := r.client.CreateDomainEntry(params)
|
||||||
|
return err
|
||||||
|
}
|
68
providers/dns/lightsail/lightsail_integration_test.go
Normal file
68
providers/dns/lightsail/lightsail_integration_test.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package lightsail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLightsailTTL(t *testing.T) {
|
||||||
|
|
||||||
|
m, err := testGetAndPreCheck()
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Fatal: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = provider.Present(m["lightsailDomain"], "foo", "bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Fatal: %s", err.Error())
|
||||||
|
}
|
||||||
|
// we need a separate Lightshail client here as the one in the DNS provider is
|
||||||
|
// unexported.
|
||||||
|
fqdn := "_acme-challenge." + m["lightsailDomain"]
|
||||||
|
svc := lightsail.New(session.New())
|
||||||
|
if err != nil {
|
||||||
|
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||||
|
t.Fatalf("Fatal: %s", err.Error())
|
||||||
|
}
|
||||||
|
params := &lightsail.GetDomainInput{
|
||||||
|
DomainName: aws.String(m["lightsailDomain"]),
|
||||||
|
}
|
||||||
|
resp, err := svc.GetDomain(params)
|
||||||
|
if err != nil {
|
||||||
|
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||||
|
t.Fatalf("Fatal: %s", err.Error())
|
||||||
|
}
|
||||||
|
entries := resp.Domain.DomainEntries
|
||||||
|
for _, entry := range entries {
|
||||||
|
if *entry.Type == "TXT" && *entry.Name == fqdn {
|
||||||
|
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provider.CleanUp(m["lightsailDomain"], "foo", "bar")
|
||||||
|
t.Fatalf("Could not find a TXT record for _acme-challenge.%s", m["lightsailDomain"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetAndPreCheck() (map[string]string, error) {
|
||||||
|
m := map[string]string{
|
||||||
|
"lightsailKey": os.Getenv("AWS_ACCESS_KEY_ID"),
|
||||||
|
"lightsailSecret": os.Getenv("AWS_SECRET_ACCESS_KEY"),
|
||||||
|
"lightsailDomain": os.Getenv("DNS_ZONE"),
|
||||||
|
}
|
||||||
|
for _, v := range m {
|
||||||
|
if v == "" {
|
||||||
|
return nil, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and R53_DOMAIN are needed to run this test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
76
providers/dns/lightsail/lightsail_test.go
Normal file
76
providers/dns/lightsail/lightsail_test.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package lightsail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/lightsail"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lightsailSecret string
|
||||||
|
lightsailKey string
|
||||||
|
lightsailZone string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
lightsailKey = os.Getenv("AWS_ACCESS_KEY_ID")
|
||||||
|
lightsailSecret = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreLightsailEnv() {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", lightsailKey)
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", lightsailSecret)
|
||||||
|
os.Setenv("AWS_REGION", "us-east-1")
|
||||||
|
os.Setenv("AWS_HOSTED_ZONE_ID", lightsailZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLightsailProvider(ts *httptest.Server) *DNSProvider {
|
||||||
|
config := &aws.Config{
|
||||||
|
Credentials: credentials.NewStaticCredentials("abc", "123", " "),
|
||||||
|
Endpoint: aws.String(ts.URL),
|
||||||
|
Region: aws.String("mock-region"),
|
||||||
|
MaxRetries: aws.Int(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
client := lightsail.New(session.New(config))
|
||||||
|
return &DNSProvider{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCredentialsFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", "123")
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "123")
|
||||||
|
os.Setenv("AWS_REGION", "us-east-1")
|
||||||
|
|
||||||
|
config := &aws.Config{
|
||||||
|
CredentialsChainVerboseErrors: aws.Bool(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := session.New(config)
|
||||||
|
_, err := sess.Config.Credentials.Get()
|
||||||
|
assert.NoError(t, err, "Expected credentials to be set from environment")
|
||||||
|
|
||||||
|
restoreLightsailEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLightsailPresent(t *testing.T) {
|
||||||
|
mockResponses := MockResponseMap{
|
||||||
|
"/": MockResponse{StatusCode: 200, Body: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := newMockServer(t, mockResponses)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
provider := makeLightsailProvider(ts)
|
||||||
|
|
||||||
|
domain := "example.com"
|
||||||
|
keyAuth := "123456d=="
|
||||||
|
|
||||||
|
err := provider.Present(domain, "", keyAuth)
|
||||||
|
assert.NoError(t, err, "Expected Present to return no error")
|
||||||
|
}
|
38
providers/dns/lightsail/testutil_test.go
Normal file
38
providers/dns/lightsail/testutil_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package lightsail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockResponse represents a predefined response used by a mock server
|
||||||
|
type MockResponse struct {
|
||||||
|
StatusCode int
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockResponseMap maps request paths to responses
|
||||||
|
type MockResponseMap map[string]MockResponse
|
||||||
|
|
||||||
|
func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := r.URL.Path
|
||||||
|
resp, ok := responses[path]
|
||||||
|
if !ok {
|
||||||
|
msg := fmt.Sprintf("Requested path not found in response map: %s", path)
|
||||||
|
require.FailNow(t, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
w.Write([]byte(resp.Body))
|
||||||
|
}))
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
return ts
|
||||||
|
}
|
124
providers/dns/namedotcom/namedotcom.go
Normal file
124
providers/dns/namedotcom/namedotcom.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
// Package namedotcom implements a DNS provider for solving the DNS-01 challenge
|
||||||
|
// using Name.com's DNS service.
|
||||||
|
package namedotcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/namedotcom/go/namecom"
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
|
||||||
|
type DNSProvider struct {
|
||||||
|
client *namecom.NameCom
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for namedotcom.
|
||||||
|
// Credentials must be passed in the environment variables: NAMECOM_USERNAME and NAMECOM_API_TOKEN
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
username := os.Getenv("NAMECOM_USERNAME")
|
||||||
|
apiToken := os.Getenv("NAMECOM_API_TOKEN")
|
||||||
|
server := os.Getenv("NAMECOM_SERVER")
|
||||||
|
|
||||||
|
return NewDNSProviderCredentials(username, apiToken, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
|
// DNSProvider instance configured for namedotcom.
|
||||||
|
func NewDNSProviderCredentials(username, apiToken, server string) (*DNSProvider, error) {
|
||||||
|
if username == "" {
|
||||||
|
return nil, fmt.Errorf("Name.com Username is required")
|
||||||
|
}
|
||||||
|
if apiToken == "" {
|
||||||
|
return nil, fmt.Errorf("Name.com API token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := namecom.New(username, apiToken)
|
||||||
|
|
||||||
|
if server != "" {
|
||||||
|
client.Server = server
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||||
|
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
request := &namecom.Record{
|
||||||
|
DomainName: domain,
|
||||||
|
Host: c.extractRecordName(fqdn, domain),
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: uint32(ttl),
|
||||||
|
Answer: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.client.CreateRecord(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("namedotcom API call failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
records, err := c.getRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if rec.Fqdn == fqdn && rec.Type == "TXT" {
|
||||||
|
request := &namecom.DeleteRecordRequest{
|
||||||
|
DomainName: domain,
|
||||||
|
ID: rec.ID,
|
||||||
|
}
|
||||||
|
_, err := c.client.DeleteRecord(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
records []*namecom.Record
|
||||||
|
response *namecom.ListRecordsResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
request := &namecom.ListRecordsRequest{
|
||||||
|
DomainName: domain,
|
||||||
|
Page: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for request.Page > 0 {
|
||||||
|
response, err = c.client.ListRecords(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, response.Records...)
|
||||||
|
request.Page = response.NextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
|
||||||
|
name := acme.UnFqdn(fqdn)
|
||||||
|
if idx := strings.Index(name, "."+domain); idx != -1 {
|
||||||
|
return name[:idx]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
58
providers/dns/namedotcom/namedotcom_test.go
Normal file
58
providers/dns/namedotcom/namedotcom_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package namedotcom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
namedotcomLiveTest bool
|
||||||
|
namedotcomUsername string
|
||||||
|
namedotcomAPIToken string
|
||||||
|
namedotcomDomain string
|
||||||
|
namedotcomServer string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
namedotcomUsername = os.Getenv("NAMEDOTCOM_USERNAME")
|
||||||
|
namedotcomAPIToken = os.Getenv("NAMEDOTCOM_API_TOKEN")
|
||||||
|
namedotcomDomain = os.Getenv("NAMEDOTCOM_DOMAIN")
|
||||||
|
namedotcomServer = os.Getenv("NAMEDOTCOM_SERVER")
|
||||||
|
|
||||||
|
if len(namedotcomAPIToken) > 0 && len(namedotcomUsername) > 0 && len(namedotcomDomain) > 0 {
|
||||||
|
namedotcomLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLivenamedotcomPresent(t *testing.T) {
|
||||||
|
if !namedotcomLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(namedotcomDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Cleanup
|
||||||
|
//
|
||||||
|
|
||||||
|
func TestLivenamedotcomCleanUp(t *testing.T) {
|
||||||
|
if !namedotcomLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(namedotcomDomain, "", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -70,7 +70,11 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
r := customRetryer{}
|
r := customRetryer{}
|
||||||
r.NumMaxRetries = maxRetries
|
r.NumMaxRetries = maxRetries
|
||||||
config := request.WithRetryer(aws.NewConfig(), r)
|
config := request.WithRetryer(aws.NewConfig(), r)
|
||||||
client := route53.New(session.New(config))
|
session, err := session.NewSessionWithOptions(session.Options{Config: *config})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := route53.New(session)
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
client: client,
|
client: client,
|
||||||
|
|
Loading…
Reference in a new issue