Files
realDynDNS/pkg/dnsProvider/ionos/api/ionosAPI.go
Timo Behrendt b781399b47
All checks were successful
CD / test (push) Successful in 5m33s
CD / Build and push (push) Successful in 4m7s
fix: only query relevant records when looking up record ids on Ionos API (#19)
Reviewed-on: #19
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2024-12-27 16:55:26 +01:00

225 lines
5.3 KiB
Go

package ionosAPI
import (
"bytes"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/url"
"realdnydns/model/common"
)
/**
* Docs: https://developer.hosting.ionos.com/docs/dns
*/
type IonosAPI interface {
GetARecord(tld string, subdomain string) (*common.ARecord, error)
SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
GetZoneId(tld string) (string, error)
GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error)
HttpCall(method string, url string, body io.Reader, queryParams map[string]string) (*http.Response, error)
}
type IonosAPIImpl struct {
APIKey string
BaseURL string
HTTPClient *http.Client
}
type ZonesResponse []struct {
Name string `json:"name"`
Id string `json:"id"`
Type string `json:"type"`
}
type ZoneResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Records []RecordResponse `json:"records"`
}
type RecordResponse struct {
Id string `json:"id"`
Name string `json:"name"`
RootName string `json:"rootName"`
Type string `json:"type"`
Content string `json:"content"`
ChangeDate string `json:"changeDate"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
type ChangeRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
type ChangeRecordRequest struct {
Content string `json:"content"`
TTL int `json:"ttl"`
Prio int `json:"prio"`
Disabled bool `json:"disabled"`
}
func New(APIKey string, BaseURL string) IonosAPI {
return &IonosAPIImpl{
APIKey: APIKey,
BaseURL: BaseURL,
HTTPClient: &http.Client{},
}
}
func (i *IonosAPIImpl) HttpCall(method string, path string, body io.Reader, queryParams map[string]string) (*http.Response, error) {
requestUrl, _ := url.Parse(i.BaseURL + path)
query := requestUrl.Query()
for key, value := range queryParams {
query.Add(key, value)
}
requestUrl.RawQuery = query.Encode()
req, err := http.NewRequest(method, requestUrl.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("X-API-Key", i.APIKey)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
return i.HTTPClient.Do(req)
}
func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
res, err := i.HttpCall("GET", "/v1/zones", nil, nil)
if err != nil {
return "", err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
zones := []ZoneResponse{}
json.Unmarshal(responseBody, &zones)
for _, z := range zones {
if z.Name == tld {
return z.Id, nil
}
}
return "", errors.New("zone not found")
}
func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) {
var domain string
if subdomain == "@" || subdomain == "" {
domain = tld
} else {
domain = subdomain + "." + tld
}
res, err := i.HttpCall("GET", "/v1/zones/"+zoneId, nil, map[string]string{"recordName": domain, "recordType": recordType})
if err != nil {
return "", err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
zone := ZoneResponse{}
json.Unmarshal(responseBody, &zone)
for _, record := range zone.Records {
if record.Type == recordType && record.Name == domain {
return record.Id, nil
}
}
return "", errors.New("record not found")
}
func (i *IonosAPIImpl) SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
zoneId, err := i.GetZoneId(tld)
if err != nil {
return nil, err
}
recordId, err := i.GetRecordId(zoneId, tld, subdomain, "A")
if err != nil {
return nil, err
}
changeRecordRequest, err := json.Marshal(ChangeRecordRequest{
Content: ip.String(),
TTL: ttl,
Prio: prio,
Disabled: false,
})
if err != nil {
return nil, err
}
res, err := i.HttpCall("PUT", "/v1/zones/"+zoneId+"/records/"+recordId, bytes.NewReader(changeRecordRequest), nil)
if err != nil {
return nil, err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
if res.StatusCode != 200 {
return nil, errors.New("error updating record")
}
changeRecord := ChangeRecord{}
json.Unmarshal(responseBody, &changeRecord)
return &common.ARecord{
Domain: changeRecord.Name,
IP: changeRecord.Content,
TTL: changeRecord.TTL,
Prio: changeRecord.Prio,
Disabled: changeRecord.Disabled,
}, nil
}
func (ionos *IonosAPIImpl) GetARecord(tld string, subdomain string) (*common.ARecord, error) {
zoneId, err := ionos.GetZoneId(tld)
if err != nil {
return nil, err
}
recordId, err := ionos.GetRecordId(zoneId, tld, subdomain, "A")
if err != nil {
return nil, err
}
res, err := ionos.HttpCall("GET", "/v1/zones/"+zoneId+"/records/"+recordId, nil, nil)
if err != nil {
return nil, err
}
responseBody := make([]byte, res.ContentLength)
res.Body.Read(responseBody)
record := RecordResponse{}
json.Unmarshal(responseBody, &record)
return &common.ARecord{
Domain: record.Name,
IP: record.Content,
TTL: record.TTL,
Prio: record.Prio,
Disabled: record.Disabled,
}, nil
}