MVP including features: * Detect IP change * Configurable/generic external IP provider * Configurable/generic DNS provider * Impl. of plain external IP provider * Impl. of Ionos DNS provider Reviewed-on: #1 Co-authored-by: Timo Behrendt <t.behrendt@t00n.de> Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
214 lines
4.9 KiB
Go
214 lines
4.9 KiB
Go
package ionosAPI
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"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) (*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, url string, body io.Reader) (*http.Response, error) {
|
|
req, err := http.NewRequest(method, i.BaseURL+url, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Add("X-API-Key", i.APIKey)
|
|
|
|
return i.HTTPClient.Do(req)
|
|
}
|
|
|
|
func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
|
|
res, err := i.HttpCall("GET", "/v1/zones", 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) {
|
|
res, err := i.HttpCall("GET", "/v1/zones/"+zoneId, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
responseBody := make([]byte, res.ContentLength)
|
|
res.Body.Read(responseBody)
|
|
|
|
zone := ZoneResponse{}
|
|
json.Unmarshal(responseBody, &zone)
|
|
|
|
var domain string
|
|
if subdomain != "" {
|
|
domain = subdomain + "." + tld
|
|
} else {
|
|
domain = tld
|
|
}
|
|
|
|
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))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
|
return nil, errors.New("error updating record")
|
|
}
|
|
|
|
responseBody := make([]byte, res.ContentLength)
|
|
res.Body.Read(responseBody)
|
|
|
|
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)
|
|
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
|
|
}
|