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, "\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, "\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, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
|
||||
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
|
||||
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN")
|
||||
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, "\tgandi:\tGANDI_API_KEY")
|
||||
fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY")
|
||||
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, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE")
|
||||
fmt.Fprintln(w, "\tmanual:\tnone")
|
||||
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, "\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")
|
||||
|
@ -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, "\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, "\texec:\tEXEC_PATH")
|
||||
w.Flush()
|
||||
|
||||
fmt.Println(`
|
||||
|
|
|
@ -27,7 +27,6 @@ type DNSProvider struct {
|
|||
subscriptionId string
|
||||
tenantId string
|
||||
resourceGroup string
|
||||
|
||||
context context.Context
|
||||
}
|
||||
|
||||
|
@ -47,7 +46,13 @@ func NewDNSProvider() (*DNSProvider, error) {
|
|||
// DNSProvider instance configured for azure.
|
||||
func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) {
|
||||
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{
|
||||
|
|
|
@ -58,7 +58,7 @@ func TestNewDNSProviderValidEnv(t *testing.T) {
|
|||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||
os.Setenv("AZURE_SUBSCRIPTION_ID", "")
|
||||
_, 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()
|
||||
}
|
||||
|
||||
|
|
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"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
|
@ -37,7 +38,14 @@ func NewDNSProvider() (*DNSProvider, error) {
|
|||
// DNSProvider instance configured for cloudflare.
|
||||
func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
|
||||
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{
|
||||
|
|
|
@ -49,7 +49,14 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
|||
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||
_, 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -13,14 +13,20 @@ import (
|
|||
"github.com/xenolf/lego/providers/dns/dnsimple"
|
||||
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
|
||||
"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/exec"
|
||||
"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/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/googlecloud"
|
||||
"github.com/xenolf/lego/providers/dns/lightsail"
|
||||
"github.com/xenolf/lego/providers/dns/linode"
|
||||
"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/otc"
|
||||
"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/route53"
|
||||
"github.com/xenolf/lego/providers/dns/vultr"
|
||||
"github.com/xenolf/lego/providers/dns/bluecat"
|
||||
)
|
||||
|
||||
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
|
||||
|
@ -39,6 +46,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||
provider, err = azure.NewDNSProvider()
|
||||
case "auroradns":
|
||||
provider, err = auroradns.NewDNSProvider()
|
||||
case "bluecat":
|
||||
provider, err = bluecat.NewDNSProvider()
|
||||
case "cloudflare":
|
||||
provider, err = cloudflare.NewDNSProvider()
|
||||
case "cloudxns":
|
||||
|
@ -51,24 +60,34 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||
provider, err = dnsmadeeasy.NewDNSProvider()
|
||||
case "dnspod":
|
||||
provider, err = dnspod.NewDNSProvider()
|
||||
case "duckdns":
|
||||
provider, err = duckdns.NewDNSProvider()
|
||||
case "dyn":
|
||||
provider, err = dyn.NewDNSProvider()
|
||||
case "fastdns":
|
||||
provider, err = fastdns.NewDNSProvider()
|
||||
case "exoscale":
|
||||
provider, err = exoscale.NewDNSProvider()
|
||||
case "gandi":
|
||||
provider, err = gandi.NewDNSProvider()
|
||||
case "gandiv5":
|
||||
provider, err = gandiv5.NewDNSProvider()
|
||||
case "glesys":
|
||||
provider, err = glesys.NewDNSProvider()
|
||||
case "gcloud":
|
||||
provider, err = googlecloud.NewDNSProvider()
|
||||
case "godaddy":
|
||||
provider, err = godaddy.NewDNSProvider()
|
||||
case "lightsail":
|
||||
provider, err = lightsail.NewDNSProvider()
|
||||
case "linode":
|
||||
provider, err = linode.NewDNSProvider()
|
||||
case "manual":
|
||||
provider, err = acme.NewDNSProviderManual()
|
||||
case "namecheap":
|
||||
provider, err = namecheap.NewDNSProvider()
|
||||
case "namedotcom":
|
||||
provider, err = namedotcom.NewDNSProvider()
|
||||
case "rackspace":
|
||||
provider, err = rackspace.NewDNSProvider()
|
||||
case "route53":
|
||||
|
@ -85,6 +104,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||
provider, err = ns1.NewDNSProvider()
|
||||
case "otc":
|
||||
provider, err = otc.NewDNSProvider()
|
||||
case "exec":
|
||||
provider, err = exec.NewDNSProvider()
|
||||
default:
|
||||
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 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()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
if resp.StatusCode >= 500 {
|
||||
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
|
||||
|
@ -100,6 +97,13 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
|
|||
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" {
|
||||
// TODO add better error handling
|
||||
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.NumMaxRetries = maxRetries
|
||||
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{
|
||||
client: client,
|
||||
|
|
Loading…
Reference in a new issue