lego/providers/dns/pdns/client.go
tbe b668bde5e4 pdns: fix wildcard with SANs (#837)
The current implementation of the DNS challenge does not allow
to set multiple TXT records at once.

As PowerDNS has the concept of record sets, and so all records
for the same type and name must set during one call, we would override
existing records.

To avoid this, we merge the new TXT record with existing ones
2019-03-21 15:46:21 +01:00

220 lines
4.4 KiB
Go

package pdns
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/go-acme/lego/challenge/dns01"
)
type Record struct {
Content string `json:"content"`
Disabled bool `json:"disabled"`
// pre-v1 API
Name string `json:"name"`
Type string `json:"type"`
TTL int `json:"ttl,omitempty"`
}
type hostedZone struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
RRSets []rrSet `json:"rrsets"`
// pre-v1 API
Records []Record `json:"records"`
}
type rrSet struct {
Name string `json:"name"`
Type string `json:"type"`
Kind string `json:"kind"`
ChangeType string `json:"changetype"`
Records []Record `json:"records"`
TTL int `json:"ttl,omitempty"`
}
type rrSets struct {
RRSets []rrSet `json:"rrsets"`
}
type apiError struct {
ShortMsg string `json:"error"`
}
func (a apiError) Error() string {
return a.ShortMsg
}
type apiVersion struct {
URL string `json:"url"`
Version int `json:"version"`
}
func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
var zone hostedZone
authZone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return nil, err
}
u := "/servers/localhost/zones"
result, err := d.sendRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
var zones []hostedZone
err = json.Unmarshal(result, &zones)
if err != nil {
return nil, err
}
u = ""
for _, zone := range zones {
if dns01.UnFqdn(zone.Name) == dns01.UnFqdn(authZone) {
u = zone.URL
break
}
}
result, err = d.sendRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
err = json.Unmarshal(result, &zone)
if err != nil {
return nil, err
}
// convert pre-v1 API result
if len(zone.Records) > 0 {
zone.RRSets = []rrSet{}
for _, record := range zone.Records {
set := rrSet{
Name: record.Name,
Type: record.Type,
Records: []Record{record},
}
zone.RRSets = append(zone.RRSets, set)
}
}
return &zone, nil
}
func (d *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) {
zone, err := d.getHostedZone(fqdn)
if err != nil {
return nil, err
}
_, err = d.sendRequest(http.MethodGet, zone.URL, nil)
if err != nil {
return nil, err
}
for _, set := range zone.RRSets {
if (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" {
return &set, nil
}
}
return nil, nil
}
func (d *DNSProvider) getAPIVersion() (int, error) {
result, err := d.sendRequest(http.MethodGet, "/api", nil)
if err != nil {
return 0, err
}
var versions []apiVersion
err = json.Unmarshal(result, &versions)
if err != nil {
return 0, err
}
latestVersion := 0
for _, v := range versions {
if v.Version > latestVersion {
latestVersion = v.Version
}
}
return latestVersion, err
}
func (d *DNSProvider) sendRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
req, err := d.makeRequest(method, uri, body)
if err != nil {
return nil, err
}
resp, err := d.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error talking to PDNS API -> %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
return nil, fmt.Errorf("unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, req.URL)
}
var msg json.RawMessage
err = json.NewDecoder(resp.Body).Decode(&msg)
if err != nil {
if err == io.EOF {
// empty body
return nil, nil
}
// other error
return nil, err
}
// check for PowerDNS error message
if len(msg) > 0 && msg[0] == '{' {
var errInfo apiError
err = json.Unmarshal(msg, &errInfo)
if err != nil {
return nil, err
}
if errInfo.ShortMsg != "" {
return nil, fmt.Errorf("error talking to PDNS API -> %v", errInfo)
}
}
return msg, nil
}
func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Request, error) {
var path = ""
if d.config.Host.Path != "/" {
path = d.config.Host.Path
}
if !strings.HasPrefix(uri, "/") {
uri = "/" + uri
}
if d.apiVersion > 0 && !strings.HasPrefix(uri, "/api/v") {
uri = "/api/v" + strconv.Itoa(d.apiVersion) + uri
}
u := d.config.Host.Scheme + "://" + d.config.Host.Host + path + uri
req, err := http.NewRequest(method, u, body)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", d.config.APIKey)
return req, nil
}