2e0e9cd68f
If Dyn responds with a 3xx or 4xx status code, information describing exactly what went wrong is generally included in the body of the response (as part of the typical Dyn JSON response). On the other hand, if Dyn responds with a 5xx status code, we very likely have extremely limited information. This commit modifies the reporting to display the explanatory messages included in the body of the Dyn response for 3xx and 4xx status codes. The intent is to make it much easier to determine what might be going wrong (when something is going wrong).
278 lines
6.6 KiB
Go
278 lines
6.6 KiB
Go
// Package dyn implements a DNS provider for solving the DNS-01 challenge
|
|
// using Dyn Managed DNS.
|
|
package dyn
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/xenolf/lego/acme"
|
|
)
|
|
|
|
var dynBaseURL = "https://api.dynect.net/REST"
|
|
|
|
type dynResponse struct {
|
|
// One of 'success', 'failure', or 'incomplete'
|
|
Status string `json:"status"`
|
|
|
|
// The structure containing the actual results of the request
|
|
Data json.RawMessage `json:"data"`
|
|
|
|
// The ID of the job that was created in response to a request.
|
|
JobID int `json:"job_id"`
|
|
|
|
// A list of zero or more messages
|
|
Messages json.RawMessage `json:"msgs"`
|
|
}
|
|
|
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
|
|
// Dyn's Managed DNS API to manage TXT records for a domain.
|
|
type DNSProvider struct {
|
|
customerName string
|
|
userName string
|
|
password string
|
|
token string
|
|
}
|
|
|
|
// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
|
|
// Credentials must be passed in the environment variables: DYN_CUSTOMER_NAME,
|
|
// DYN_USER_NAME and DYN_PASSWORD.
|
|
func NewDNSProvider() (*DNSProvider, error) {
|
|
customerName := os.Getenv("DYN_CUSTOMER_NAME")
|
|
userName := os.Getenv("DYN_USER_NAME")
|
|
password := os.Getenv("DYN_PASSWORD")
|
|
return NewDNSProviderCredentials(customerName, userName, password)
|
|
}
|
|
|
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
|
// DNSProvider instance configured for Dyn DNS.
|
|
func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) {
|
|
if customerName == "" || userName == "" || password == "" {
|
|
return nil, fmt.Errorf("DynDNS credentials missing")
|
|
}
|
|
|
|
return &DNSProvider{
|
|
customerName: customerName,
|
|
userName: userName,
|
|
password: password,
|
|
}, nil
|
|
}
|
|
|
|
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
|
|
url := fmt.Sprintf("%s/%s", dynBaseURL, 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("Auth-Token", d.token)
|
|
}
|
|
|
|
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 >= 500 {
|
|
return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode)
|
|
}
|
|
|
|
var dynRes dynResponse
|
|
err = json.NewDecoder(resp.Body).Decode(&dynRes)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
|
|
return &dynRes, nil
|
|
}
|
|
|
|
// Starts a new Dyn API Session. Authenticates using customerName, userName,
|
|
// password and receives a token to be used in for subsequent requests.
|
|
func (d *DNSProvider) login() error {
|
|
type creds struct {
|
|
Customer string `json:"customer_name"`
|
|
User string `json:"user_name"`
|
|
Pass string `json:"password"`
|
|
}
|
|
|
|
type session struct {
|
|
Token string `json:"token"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
payload := &creds{Customer: d.customerName, User: d.userName, Pass: d.password}
|
|
dynRes, err := d.sendRequest("POST", "Session", payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var s session
|
|
err = json.Unmarshal(dynRes.Data, &s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.token = s.Token
|
|
|
|
return nil
|
|
}
|
|
|
|
// Destroys Dyn Session
|
|
func (d *DNSProvider) logout() error {
|
|
if len(d.token) == 0 {
|
|
// nothing to do
|
|
return nil
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/Session", dynBaseURL)
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Auth-Token", d.token)
|
|
|
|
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("Dyn API request failed to delete session with HTTP status code %d", resp.StatusCode)
|
|
}
|
|
|
|
d.token = ""
|
|
|
|
return 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)
|
|
|
|
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.login()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"rdata": map[string]string{
|
|
"txtdata": value,
|
|
},
|
|
"ttl": strconv.Itoa(ttl),
|
|
}
|
|
|
|
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
|
|
_, err = d.sendRequest("POST", resource, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.logout()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *DNSProvider) publish(zone, notes string) error {
|
|
type publish struct {
|
|
Publish bool `json:"publish"`
|
|
Notes string `json:"notes"`
|
|
}
|
|
|
|
pub := &publish{Publish: true, Notes: notes}
|
|
resource := fmt.Sprintf("Zone/%s/", zone)
|
|
_, err := d.sendRequest("PUT", resource, pub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
|
|
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.login()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
|
|
url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Auth-Token", d.token)
|
|
|
|
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("Dyn API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
|
|
}
|
|
|
|
err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.logout()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|