forked from TrueCloudLab/lego
hostingde: fix client fails if customer has no access to dns-groups (#809)
This commit is contained in:
parent
7f6b708439
commit
a144800896
3 changed files with 268 additions and 124 deletions
|
@ -2,112 +2,102 @@ package hostingde
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json"
|
||||
|
||||
// RecordsAddRequest represents a DNS record to add
|
||||
type RecordsAddRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
// https://www.hosting.de/api/?json#list-zoneconfigs
|
||||
func (d *DNSProvider) listZoneConfigs(findRequest ZoneConfigsFindRequest) (*ZoneConfigsFindResponse, error) {
|
||||
uri := defaultBaseURL + "/zoneConfigsFind"
|
||||
|
||||
// RecordsDeleteRequest represents a DNS record to remove
|
||||
type RecordsDeleteRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
findResponse := &ZoneConfigsFindResponse{}
|
||||
|
||||
// ZoneConfigObject represents the ZoneConfig-section of a hosting.de API response.
|
||||
type ZoneConfigObject struct {
|
||||
AccountID string `json:"accountId"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
ID string `json:"id"`
|
||||
LastChangeDate string `json:"lastChangeDate"`
|
||||
MasterIP string `json:"masterIp"`
|
||||
Name string `json:"name"`
|
||||
NameUnicode string `json:"nameUnicode"`
|
||||
SOAValues struct {
|
||||
Expire int `json:"expire"`
|
||||
NegativeTTL int `json:"negativeTtl"`
|
||||
Refresh int `json:"refresh"`
|
||||
Retry int `json:"retry"`
|
||||
Serial string `json:"serial"`
|
||||
TTL int `json:"ttl"`
|
||||
} `json:"soaValues"`
|
||||
Status string `json:"status"`
|
||||
TemplateValues string `json:"templateValues"`
|
||||
Type string `json:"type"`
|
||||
ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"`
|
||||
}
|
||||
|
||||
// ZoneUpdateError represents an error in a ZoneUpdateResponse
|
||||
type ZoneUpdateError struct {
|
||||
Code int `json:"code"`
|
||||
ContextObject string `json:"contextObject"`
|
||||
ContextPath string `json:"contextPath"`
|
||||
Details []string `json:"details"`
|
||||
Text string `json:"text"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ZoneUpdateMetadata represents the metadata in a ZoneUpdateResponse
|
||||
type ZoneUpdateMetadata struct {
|
||||
ClientTransactionID string `json:"clientTransactionId"`
|
||||
ServerTransactionID string `json:"serverTransactionId"`
|
||||
}
|
||||
|
||||
// ZoneUpdateResponse represents a response from hosting.de API
|
||||
type ZoneUpdateResponse struct {
|
||||
Errors []ZoneUpdateError `json:"errors"`
|
||||
Metadata ZoneUpdateMetadata `json:"metadata"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Status string `json:"status"`
|
||||
Response struct {
|
||||
Records []struct {
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LastChangeDate string `json:"lastChangeDate"`
|
||||
Priority int `json:"priority"`
|
||||
RecordTemplateID string `json:"recordTemplateId"`
|
||||
ZoneConfigID string `json:"zoneConfigId"`
|
||||
TTL int `json:"ttl"`
|
||||
} `json:"records"`
|
||||
ZoneConfig ZoneConfigObject `json:"zoneConfig"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
// ZoneConfigSelector represents a "minimal" ZoneConfig object used in hosting.de API requests
|
||||
type ZoneConfigSelector struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ZoneUpdateRequest represents a hosting.de API ZoneUpdate request
|
||||
type ZoneUpdateRequest struct {
|
||||
AuthToken string `json:"authToken"`
|
||||
ZoneConfigSelector `json:"zoneConfig"`
|
||||
RecordsToAdd []RecordsAddRequest `json:"recordsToAdd"`
|
||||
RecordsToDelete []RecordsDeleteRequest `json:"recordsToDelete"`
|
||||
}
|
||||
|
||||
func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) {
|
||||
body, err := json.Marshal(updateRequest)
|
||||
rawResp, err := d.post(uri, findRequest, findResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, defaultBaseURL+"/zoneUpdate", bytes.NewReader(body))
|
||||
if len(findResponse.Response.Data) == 0 {
|
||||
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(uri, rawResp))
|
||||
}
|
||||
|
||||
if findResponse.Status != "success" && findResponse.Status != "pending" {
|
||||
return findResponse, errors.New(toUnreadableBodyMessage(uri, rawResp))
|
||||
}
|
||||
|
||||
return findResponse, nil
|
||||
}
|
||||
|
||||
// https://www.hosting.de/api/?json#updating-zones
|
||||
func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) {
|
||||
uri := defaultBaseURL + "/zoneUpdate"
|
||||
|
||||
// but we'll need the ID later to delete the record
|
||||
updateResponse := &ZoneUpdateResponse{}
|
||||
|
||||
rawResp, err := d.post(uri, updateRequest, updateResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if updateResponse.Status != "success" && updateResponse.Status != "pending" {
|
||||
return nil, errors.New(toUnreadableBodyMessage(uri, rawResp))
|
||||
}
|
||||
|
||||
return updateResponse, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getZone(findRequest ZoneConfigsFindRequest) (*ZoneConfig, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var zoneConfig *ZoneConfig
|
||||
|
||||
operation := func() error {
|
||||
findResponse, err := d.listZoneConfigs(findRequest)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
if findResponse.Response.Data[0].Status != "active" {
|
||||
return fmt.Errorf("unexpected status: %q", findResponse.Response.Data[0].Status)
|
||||
}
|
||||
|
||||
zoneConfig = &findResponse.Response.Data[0]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = 3 * time.Second
|
||||
bo.MaxInterval = 10 * bo.InitialInterval
|
||||
bo.MaxElapsedTime = 100 * bo.InitialInterval
|
||||
|
||||
// retry in case the zone was edited recently and is not yet active
|
||||
err := backoff.Retry(operation, backoff.WithContext(bo, ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return zoneConfig, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) post(uri string, request interface{}, response interface{}) ([]byte, error) {
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -121,23 +111,17 @@ func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateRe
|
|||
|
||||
content, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.New(toUnreadableBodyMessage(req, content))
|
||||
return nil, errors.New(toUnreadableBodyMessage(uri, content))
|
||||
}
|
||||
|
||||
// Everything looks good; but we'll need the ID later to delete the record
|
||||
updateResponse := &ZoneUpdateResponse{}
|
||||
err = json.Unmarshal(content, updateResponse)
|
||||
err = json.Unmarshal(content, response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content))
|
||||
return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(uri, content))
|
||||
}
|
||||
|
||||
if updateResponse.Status != "success" && updateResponse.Status != "pending" {
|
||||
return updateResponse, errors.New(toUnreadableBodyMessage(req, content))
|
||||
}
|
||||
|
||||
return updateResponse, nil
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
|
||||
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
|
||||
func toUnreadableBodyMessage(uri string, rawBody []byte) string {
|
||||
return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", uri, string(rawBody))
|
||||
}
|
||||
|
|
|
@ -87,7 +87,24 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
|||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
rec := []RecordsAddRequest{{
|
||||
// get the ZoneConfig for that domain
|
||||
zonesFind := ZoneConfigsFindRequest{
|
||||
Filter: Filter{
|
||||
Field: "zoneName",
|
||||
Value: domain,
|
||||
},
|
||||
Limit: 1,
|
||||
Page: 1,
|
||||
}
|
||||
zonesFind.AuthToken = d.config.APIKey
|
||||
|
||||
zoneConfig, err := d.getZone(zonesFind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostingde: %v", err)
|
||||
}
|
||||
zoneConfig.Name = d.config.ZoneName
|
||||
|
||||
rec := []DNSRecord{{
|
||||
Type: "TXT",
|
||||
Name: dns01.UnFqdn(fqdn),
|
||||
Content: value,
|
||||
|
@ -95,12 +112,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
}}
|
||||
|
||||
req := ZoneUpdateRequest{
|
||||
AuthToken: d.config.APIKey,
|
||||
ZoneConfigSelector: ZoneConfigSelector{
|
||||
Name: d.config.ZoneName,
|
||||
},
|
||||
ZoneConfig: *zoneConfig,
|
||||
RecordsToAdd: rec,
|
||||
}
|
||||
req.AuthToken = d.config.APIKey
|
||||
|
||||
resp, err := d.updateZone(req)
|
||||
if err != nil {
|
||||
|
@ -126,35 +141,41 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
// get the record's unique ID from when we created it
|
||||
d.recordIDsMu.Lock()
|
||||
recordID, ok := d.recordIDs[fqdn]
|
||||
d.recordIDsMu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("hostingde: unknown record ID for %q", fqdn)
|
||||
}
|
||||
|
||||
rec := []RecordsDeleteRequest{{
|
||||
rec := []DNSRecord{{
|
||||
Type: "TXT",
|
||||
Name: dns01.UnFqdn(fqdn),
|
||||
Content: value,
|
||||
ID: recordID,
|
||||
Content: `"` + value + `"`,
|
||||
}}
|
||||
|
||||
req := ZoneUpdateRequest{
|
||||
AuthToken: d.config.APIKey,
|
||||
ZoneConfigSelector: ZoneConfigSelector{
|
||||
Name: d.config.ZoneName,
|
||||
// get the ZoneConfig for that domain
|
||||
zonesFind := ZoneConfigsFindRequest{
|
||||
Filter: Filter{
|
||||
Field: "zoneName",
|
||||
Value: domain,
|
||||
},
|
||||
Limit: 1,
|
||||
Page: 1,
|
||||
}
|
||||
zonesFind.AuthToken = d.config.APIKey
|
||||
|
||||
zoneConfig, err := d.getZone(zonesFind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostingde: %v", err)
|
||||
}
|
||||
zoneConfig.Name = d.config.ZoneName
|
||||
|
||||
req := ZoneUpdateRequest{
|
||||
ZoneConfig: *zoneConfig,
|
||||
RecordsToDelete: rec,
|
||||
}
|
||||
req.AuthToken = d.config.APIKey
|
||||
|
||||
// Delete record ID from map
|
||||
d.recordIDsMu.Lock()
|
||||
delete(d.recordIDs, fqdn)
|
||||
d.recordIDsMu.Unlock()
|
||||
|
||||
_, err := d.updateZone(req)
|
||||
_, err = d.updateZone(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostingde: %v", err)
|
||||
}
|
||||
|
|
139
providers/dns/hostingde/model.go
Normal file
139
providers/dns/hostingde/model.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package hostingde
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// APIError represents an error in an API response.
|
||||
// https://www.hosting.de/api/?json#warnings-and-errors
|
||||
type APIError struct {
|
||||
Code int `json:"code"`
|
||||
ContextObject string `json:"contextObject"`
|
||||
ContextPath string `json:"contextPath"`
|
||||
Details []string `json:"details"`
|
||||
Text string `json:"text"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Filter is used to filter FindRequests to the API.
|
||||
// https://www.hosting.de/api/?json#filter-object
|
||||
type Filter struct {
|
||||
Field string `json:"field"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Sort is used to sort FindRequests from the API.
|
||||
// https://www.hosting.de/api/?json#filtering-and-sorting
|
||||
type Sort struct {
|
||||
Field string `json:"zoneName"`
|
||||
Order string `json:"order"`
|
||||
}
|
||||
|
||||
// Metadata represents the metadata in an API response.
|
||||
// https://www.hosting.de/api/?json#metadata-object
|
||||
type Metadata struct {
|
||||
ClientTransactionID string `json:"clientTransactionId"`
|
||||
ServerTransactionID string `json:"serverTransactionId"`
|
||||
}
|
||||
|
||||
// ZoneConfig The ZoneConfig object defines a zone.
|
||||
// https://www.hosting.de/api/?json#the-zoneconfig-object
|
||||
type ZoneConfig struct {
|
||||
ID string `json:"id"`
|
||||
AccountID string `json:"accountId"`
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
NameUnicode string `json:"nameUnicode"`
|
||||
MasterIP string `json:"masterIp"`
|
||||
Type string `json:"type"`
|
||||
EMailAddress string `json:"emailAddress"`
|
||||
ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"`
|
||||
LastChangeDate string `json:"lastChangeDate"`
|
||||
DNSServerGroupID string `json:"dnsServerGroupId"`
|
||||
DNSSecMode string `json:"dnsSecMode"`
|
||||
SOAValues *SOAValues `json:"soaValues,omitempty"`
|
||||
TemplateValues json.RawMessage `json:"templateValues,omitempty"`
|
||||
}
|
||||
|
||||
// SOAValues The SOA values object contains the time (seconds) used in a zone’s SOA record.
|
||||
// https://www.hosting.de/api/?json#the-soa-values-object
|
||||
type SOAValues struct {
|
||||
Refresh int `json:"refresh"`
|
||||
Retry int `json:"retry"`
|
||||
Expire int `json:"expire"`
|
||||
TTL int `json:"ttl"`
|
||||
NegativeTTL int `json:"negativeTtl"`
|
||||
}
|
||||
|
||||
// DNSRecord The DNS Record object is part of a zone. It is used to manage DNS resource records.
|
||||
// https://www.hosting.de/api/?json#the-record-object
|
||||
type DNSRecord struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ZoneID string `json:"zoneId,omitempty"`
|
||||
RecordTemplateID string `json:"recordTemplateId,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
LastChangeDate string `json:"lastChangeDate,omitempty"`
|
||||
}
|
||||
|
||||
// Zone The Zone Object.
|
||||
// https://www.hosting.de/api/?json#the-zone-object
|
||||
type Zone struct {
|
||||
Records []DNSRecord `json:"records"`
|
||||
ZoneConfig ZoneConfig `json:"zoneConfig"`
|
||||
}
|
||||
|
||||
// ZoneUpdateRequest represents a API ZoneUpdate request.
|
||||
// https://www.hosting.de/api/?json#updating-zones
|
||||
type ZoneUpdateRequest struct {
|
||||
BaseRequest
|
||||
ZoneConfig `json:"zoneConfig"`
|
||||
RecordsToAdd []DNSRecord `json:"recordsToAdd"`
|
||||
RecordsToDelete []DNSRecord `json:"recordsToDelete"`
|
||||
}
|
||||
|
||||
// ZoneUpdateResponse represents a response from the API.
|
||||
// https://www.hosting.de/api/?json#updating-zones
|
||||
type ZoneUpdateResponse struct {
|
||||
BaseResponse
|
||||
Response Zone `json:"response"`
|
||||
}
|
||||
|
||||
// ZoneConfigsFindRequest represents a API ZonesFind request.
|
||||
// https://www.hosting.de/api/?json#list-zoneconfigs
|
||||
type ZoneConfigsFindRequest struct {
|
||||
BaseRequest
|
||||
Filter Filter `json:"filter"`
|
||||
Limit int `json:"limit"`
|
||||
Page int `json:"page"`
|
||||
Sort *Sort `json:"sort,omitempty"`
|
||||
}
|
||||
|
||||
// ZoneConfigsFindResponse represents the API response for ZoneConfigsFind.
|
||||
// https://www.hosting.de/api/?json#list-zoneconfigs
|
||||
type ZoneConfigsFindResponse struct {
|
||||
BaseResponse
|
||||
Response struct {
|
||||
Limit int `json:"limit"`
|
||||
Page int `json:"page"`
|
||||
TotalEntries int `json:"totalEntries"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
Type string `json:"type"`
|
||||
Data []ZoneConfig `json:"data"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
// BaseResponse Common response struct.
|
||||
// https://www.hosting.de/api/?json#responses
|
||||
type BaseResponse struct {
|
||||
Errors []APIError `json:"errors"`
|
||||
Metadata Metadata `json:"metadata"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// BaseRequest Common request struct.
|
||||
type BaseRequest struct {
|
||||
AuthToken string `json:"authToken"`
|
||||
}
|
Loading…
Reference in a new issue