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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json"
|
const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json"
|
||||||
|
|
||||||
// RecordsAddRequest represents a DNS record to add
|
// https://www.hosting.de/api/?json#list-zoneconfigs
|
||||||
type RecordsAddRequest struct {
|
func (d *DNSProvider) listZoneConfigs(findRequest ZoneConfigsFindRequest) (*ZoneConfigsFindResponse, error) {
|
||||||
Name string `json:"name"`
|
uri := defaultBaseURL + "/zoneConfigsFind"
|
||||||
Type string `json:"type"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
TTL int `json:"ttl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordsDeleteRequest represents a DNS record to remove
|
findResponse := &ZoneConfigsFindResponse{}
|
||||||
type RecordsDeleteRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZoneConfigObject represents the ZoneConfig-section of a hosting.de API response.
|
rawResp, err := d.post(uri, findRequest, findResponse)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -121,23 +111,17 @@ func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateRe
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(resp.Body)
|
content, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
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
|
err = json.Unmarshal(content, response)
|
||||||
updateResponse := &ZoneUpdateResponse{}
|
|
||||||
err = json.Unmarshal(content, updateResponse)
|
|
||||||
if err != nil {
|
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 content, nil
|
||||||
return updateResponse, errors.New(toUnreadableBodyMessage(req, content))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateResponse, nil
|
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))
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,24 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
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",
|
Type: "TXT",
|
||||||
Name: dns01.UnFqdn(fqdn),
|
Name: dns01.UnFqdn(fqdn),
|
||||||
Content: value,
|
Content: value,
|
||||||
|
@ -95,12 +112,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
}}
|
}}
|
||||||
|
|
||||||
req := ZoneUpdateRequest{
|
req := ZoneUpdateRequest{
|
||||||
AuthToken: d.config.APIKey,
|
ZoneConfig: *zoneConfig,
|
||||||
ZoneConfigSelector: ZoneConfigSelector{
|
|
||||||
Name: d.config.ZoneName,
|
|
||||||
},
|
|
||||||
RecordsToAdd: rec,
|
RecordsToAdd: rec,
|
||||||
}
|
}
|
||||||
|
req.AuthToken = d.config.APIKey
|
||||||
|
|
||||||
resp, err := d.updateZone(req)
|
resp, err := d.updateZone(req)
|
||||||
if err != nil {
|
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 {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
// get the record's unique ID from when we created it
|
rec := []DNSRecord{{
|
||||||
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{{
|
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Name: dns01.UnFqdn(fqdn),
|
Name: dns01.UnFqdn(fqdn),
|
||||||
Content: value,
|
Content: `"` + value + `"`,
|
||||||
ID: recordID,
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
req := ZoneUpdateRequest{
|
// get the ZoneConfig for that domain
|
||||||
AuthToken: d.config.APIKey,
|
zonesFind := ZoneConfigsFindRequest{
|
||||||
ZoneConfigSelector: ZoneConfigSelector{
|
Filter: Filter{
|
||||||
Name: d.config.ZoneName,
|
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,
|
RecordsToDelete: rec,
|
||||||
}
|
}
|
||||||
|
req.AuthToken = d.config.APIKey
|
||||||
|
|
||||||
// Delete record ID from map
|
// Delete record ID from map
|
||||||
d.recordIDsMu.Lock()
|
d.recordIDsMu.Lock()
|
||||||
delete(d.recordIDs, fqdn)
|
delete(d.recordIDs, fqdn)
|
||||||
d.recordIDsMu.Unlock()
|
d.recordIDsMu.Unlock()
|
||||||
|
|
||||||
_, err := d.updateZone(req)
|
_, err = d.updateZone(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("hostingde: %v", err)
|
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