Update Lego (Gandi API v5, cloudxns, ...)

This commit is contained in:
Ludovic Fernandez 2018-02-12 18:10:05 +01:00 committed by Traefiker Bot
parent dd873fbeee
commit 7d3dd5a0e4
43 changed files with 4112 additions and 1238 deletions

View file

@ -18,6 +18,7 @@ var UserAgent string
// HTTPClient is an HTTP client with a reasonable timeout value.
var HTTPClient = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,

View file

@ -0,0 +1,214 @@
// Package cloudxns implements a DNS provider for solving the DNS-01 challenge
// using cloudxns DNS.
package cloudxns
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/xenolf/lego/acme"
)
const cloudXNSBaseURL = "https://www.cloudxns.net/api2/"
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
apiKey string
secretKey string
}
// NewDNSProvider returns a DNSProvider instance configured for cloudxns.
// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY
// and CLOUDXNS_SECRET_KEY.
func NewDNSProvider() (*DNSProvider, error) {
apiKey := os.Getenv("CLOUDXNS_API_KEY")
secretKey := os.Getenv("CLOUDXNS_SECRET_KEY")
return NewDNSProviderCredentials(apiKey, secretKey)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for cloudxns.
func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) {
if apiKey == "" || secretKey == "" {
return nil, fmt.Errorf("CloudXNS credentials missing")
}
return &DNSProvider{
apiKey: apiKey,
secretKey: secretKey,
}, 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)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
}
return c.addTxtRecord(zoneID, fqdn, value, ttl)
}
// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
zoneID, err := c.getHostedZoneID(fqdn)
if err != nil {
return err
}
recordID, err := c.findTxtRecord(zoneID, fqdn)
if err != nil {
return err
}
return c.delTxtRecord(recordID, zoneID)
}
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
type Data struct {
ID string `json:"id"`
Domain string `json:"domain"`
}
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return "", err
}
result, err := c.makeRequest("GET", "domain", nil)
if err != nil {
return "", err
}
var domains []Data
err = json.Unmarshal(result, &domains)
if err != nil {
return "", err
}
for _, data := range domains {
if data.Domain == authZone {
return data.ID, nil
}
}
return "", fmt.Errorf("Zone %s not found in cloudxns for domain %s", authZone, fqdn)
}
func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) {
result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil)
if err != nil {
return "", err
}
var records []cloudXNSRecord
err = json.Unmarshal(result, &records)
if err != nil {
return "", err
}
for _, record := range records {
if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" {
return record.RecordID, nil
}
}
return "", fmt.Errorf("No existing record found for %s", fqdn)
}
func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error {
id, err := strconv.Atoi(zoneID)
if err != nil {
return err
}
payload := cloudXNSRecord{
ID: id,
Host: acme.UnFqdn(fqdn),
Value: value,
Type: "TXT",
LineID: 1,
TTL: ttl,
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
_, err = c.makeRequest("POST", "record", body)
if err != nil {
return err
}
return nil
}
func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error {
_, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil)
return err
}
func (c *DNSProvider) hmac(url, date, body string) string {
sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey))
return hex.EncodeToString(sum[:])
}
func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) {
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
}
url := cloudXNSBaseURL + uri
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
requestDate := time.Now().Format(time.RFC1123Z)
req.Header.Set("API-KEY", c.apiKey)
req.Header.Set("API-REQUEST-DATE", requestDate)
req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
req.Header.Set("API-FORMAT", "json")
resp, err := acme.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var r APIResponse
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, err
}
if r.Code != 1 {
return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message)
}
return r.Data, nil
}
type cloudXNSRecord struct {
ID int `json:"domain_id,omitempty"`
RecordID string `json:"record_id,omitempty"`
Host string `json:"host"`
Value string `json:"value"`
Type string `json:"type"`
LineID int `json:"line_id,string"`
TTL int `json:"ttl,string"`
}

View file

@ -8,6 +8,7 @@ import (
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"github.com/xenolf/lego/providers/dns/cloudflare"
"github.com/xenolf/lego/providers/dns/cloudxns"
"github.com/xenolf/lego/providers/dns/digitalocean"
"github.com/xenolf/lego/providers/dns/dnsimple"
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
@ -15,8 +16,9 @@ import (
"github.com/xenolf/lego/providers/dns/dyn"
"github.com/xenolf/lego/providers/dns/exoscale"
"github.com/xenolf/lego/providers/dns/gandi"
"github.com/xenolf/lego/providers/dns/godaddy"
"github.com/xenolf/lego/providers/dns/gandiv5"
"github.com/xenolf/lego/providers/dns/googlecloud"
"github.com/xenolf/lego/providers/dns/godaddy"
"github.com/xenolf/lego/providers/dns/linode"
"github.com/xenolf/lego/providers/dns/namecheap"
"github.com/xenolf/lego/providers/dns/ns1"
@ -39,6 +41,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
provider, err = auroradns.NewDNSProvider()
case "cloudflare":
provider, err = cloudflare.NewDNSProvider()
case "cloudxns":
provider, err = cloudxns.NewDNSProvider()
case "digitalocean":
provider, err = digitalocean.NewDNSProvider()
case "dnsimple":
@ -53,6 +57,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
provider, err = exoscale.NewDNSProvider()
case "gandi":
provider, err = gandi.NewDNSProvider()
case "gandiv5":
provider, err = gandiv5.NewDNSProvider()
case "gcloud":
provider, err = googlecloud.NewDNSProvider()
case "godaddy":

View file

@ -48,25 +48,25 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
return err
}
recordId, err := c.FindExistingRecordId(zone, recordName)
recordID, err := c.FindExistingRecordId(zone, recordName)
if err != nil {
return err
}
record := egoscale.DNSRecord{
Name: recordName,
Ttl: ttl,
TTL: ttl,
Content: value,
RecordType: "TXT",
}
if recordId == 0 {
if recordID == 0 {
_, err := c.client.CreateRecord(zone, record)
if err != nil {
return errors.New("Error while creating DNS record: " + err.Error())
}
} else {
record.Id = recordId
record.ID = recordID
_, err := c.client.UpdateRecord(zone, record)
if err != nil {
return errors.New("Error while updating DNS record: " + err.Error())
@ -84,17 +84,13 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return err
}
recordId, err := c.FindExistingRecordId(zone, recordName)
recordID, err := c.FindExistingRecordId(zone, recordName)
if err != nil {
return err
}
if recordId != 0 {
record := egoscale.DNSRecord{
Id: recordId,
}
err = c.client.DeleteRecord(zone, record)
if recordID != 0 {
err = c.client.DeleteRecord(zone, recordID)
if err != nil {
return errors.New("Error while deleting DNS record: " + err.Error())
}
@ -106,13 +102,13 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
// Query Exoscale to find an existing record for this name.
// Returns nil if no record could be found
func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, error) {
responses, err := c.client.GetRecords(zone)
records, err := c.client.GetRecords(zone)
if err != nil {
return -1, errors.New("Error while retrievening DNS records: " + err.Error())
}
for _, response := range responses {
if response.Record.Name == recordName {
return response.Record.Id, nil
for _, record := range records {
if record.Name == recordName {
return record.ID, nil
}
}
return 0, nil

View file

@ -0,0 +1,203 @@
// Package gandiv5 implements a DNS provider for solving the DNS-01
// challenge using Gandi LiveDNS api.
package gandiv5
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/xenolf/lego/acme"
)
// Gandi API reference: http://doc.livedns.gandi.net/
var (
// endpoint is the Gandi API endpoint used by Present and
// CleanUp. It is overridden during tests.
endpoint = "https://dns.api.gandi.net/api/v5"
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
// during tests.
findZoneByFqdn = acme.FindZoneByFqdn
)
// inProgressInfo contains information about an in-progress challenge
type inProgressInfo struct {
fieldName string
authZone string
}
// DNSProvider is an implementation of the
// acme.ChallengeProviderTimeout interface that uses Gandi's LiveDNS
// API to manage TXT records for a domain.
type DNSProvider struct {
apiKey string
inProgressFQDNs map[string]inProgressInfo
inProgressMu sync.Mutex
}
// NewDNSProvider returns a DNSProvider instance configured for Gandi.
// Credentials must be passed in the environment variable: GANDIV5_API_KEY.
func NewDNSProvider() (*DNSProvider, error) {
apiKey := os.Getenv("GANDIV5_API_KEY")
return NewDNSProviderCredentials(apiKey)
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for Gandi.
func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
if apiKey == "" {
return nil, fmt.Errorf("Gandi DNS: No Gandi API Key given")
}
return &DNSProvider{
apiKey: apiKey,
inProgressFQDNs: make(map[string]inProgressInfo),
}, 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 < 300 {
ttl = 300 // 300 is gandi minimum value for ttl
}
// find authZone
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
}
// determine name of TXT record
if !strings.HasSuffix(
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
return fmt.Errorf(
"Gandi 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
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl)
if err != nil {
return err
}
// save data necessary for CleanUp
d.inProgressFQDNs[fqdn] = inProgressInfo{
authZone: authZone,
fieldName: name,
}
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.inProgressFQDNs[fqdn]; !ok {
// if there is no cleanup information then just return
return nil
}
fieldName := d.inProgressFQDNs[fqdn].fieldName
authZone := d.inProgressFQDNs[fqdn].authZone
delete(d.inProgressFQDNs, fqdn)
// delete TXT record from authZone
err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
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 Gandi.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 20 * time.Minute, 20 * time.Second
}
// types for JSON method calls and parameters
type addFieldRequest struct {
RRSetTTL int `json:"rrset_ttl"`
RRSetValues []string `json:"rrset_values"`
}
type deleteFieldRequest struct {
Delete bool `json:"delete"`
}
// types for JSON responses
type responseStruct struct {
Message string `json:"message"`
}
// POSTing/Marshalling/Unmarshalling
func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) {
url := fmt.Sprintf("%s/%s", endpoint, 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.apiKey) > 0 {
req.Header.Set("X-Api-Key", 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("Gandi DNS: request failed with HTTP status code %d", resp.StatusCode)
}
var response responseStruct
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil && method != "DELETE" {
return nil, err
}
return &response, nil
}
// functions to perform API actions
func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest("PUT", target, addFieldRequest{
RRSetTTL: ttl,
RRSetValues: []string{value},
})
if response != nil {
fmt.Printf("Gandi DNS: %s\n", response.Message)
}
return err
}
func (d *DNSProvider) deleteTXTRecord(domain string, name string) error {
target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name)
response, err := d.sendRequest("DELETE", target, deleteFieldRequest{
Delete: true,
})
if response != nil && response.Message == "" {
fmt.Printf("Gandi DNS: Zone record deleted\n")
}
return err
}

View file

@ -257,12 +257,11 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
if c.host.Path != "/" {
path = c.host.Path
}
if c.apiVersion > 0 {
if !strings.HasPrefix(uri, "api/v") {
uri = "/api/v" + strconv.Itoa(c.apiVersion) + uri
} else {
uri = "/" + uri
}
if !strings.HasPrefix(uri, "/") {
uri = "/" + uri
}
if c.apiVersion > 0 && !strings.HasPrefix(uri, "/api/v") {
uri = "/api/v" + strconv.Itoa(c.apiVersion) + uri
}
url := c.host.Scheme + "://" + c.host.Host + path + uri
req, err := http.NewRequest(method, url, body)