2021-03-09 08:25:55 +08:00
|
|
|
package hostingde
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errZoneNotFound = errors.Errorf("zone not found")
|
|
|
|
)
|
|
|
|
|
|
|
|
type request struct {
|
|
|
|
AuthToken string `json:"authToken"`
|
|
|
|
OwnerAccountID string `json:"ownerAccountId,omitempty"`
|
|
|
|
Filter filter `json:"filter,omitempty"`
|
|
|
|
Limit uint `json:"limit,omitempty"`
|
|
|
|
Page uint `json:"page,omitempty"`
|
|
|
|
|
|
|
|
// Update Zone
|
|
|
|
ZoneConfig *zoneConfig `json:"zoneConfig"`
|
|
|
|
RecordsToAdd []*record `json:"recordsToAdd"`
|
|
|
|
RecordsToModify []*record `json:"recordsToModify"`
|
|
|
|
RecordsToDelete []*record `json:"recordsToDelete"`
|
|
|
|
|
|
|
|
// Create Zone
|
|
|
|
Records []*record `json:"records"`
|
|
|
|
|
|
|
|
// Domain
|
|
|
|
Domain *domainConfig `json:"domain"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type filter struct {
|
|
|
|
Field string `json:"field"`
|
|
|
|
Value string `json:"value"`
|
|
|
|
Relation string `json:"relation,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type nameserver struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
IPs []net.IP `json:"ips"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type domainConfig struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Contacts json.RawMessage `json:"contacts"`
|
|
|
|
Nameservers []nameserver `json:"nameservers"`
|
|
|
|
TransferLockEnabled bool `json:"transferLockEnabled"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type zoneConfig struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
DNSSECMode string `json:"dnsSecMode"`
|
|
|
|
EmailAddress string `json:"emailAddress,omitempty"`
|
|
|
|
MasterIP string `json:"masterIp"`
|
|
|
|
Name string `json:"name"` // Not required per docs, but required IRL
|
|
|
|
NameUnicode string `json:"nameUnicode"`
|
|
|
|
// SOAValues struct {
|
|
|
|
// Refresh uint32 `json:"refresh"`
|
|
|
|
// Retry uint32 `json:"retry"`
|
|
|
|
// Expire uint32 `json:"expire"`
|
|
|
|
// TTL uint32 `json:"ttl"`
|
|
|
|
// NegativeTTL uint32 `json:"negativeTtl"`
|
|
|
|
// } `json:"soaValues,omitempty"`
|
|
|
|
Type string `json:"type"`
|
|
|
|
ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type record struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Type string `json:"type"`
|
|
|
|
Content string `json:"content"`
|
|
|
|
TTL uint32 `json:"ttl"`
|
|
|
|
Priority uint16 `json:"priority"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type response struct {
|
|
|
|
Errors []apiError `json:"errors"`
|
|
|
|
Response *responseData `json:"response"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type apiError struct {
|
|
|
|
Code int `json:"code"`
|
|
|
|
ContextObject string `json:"contextObject"`
|
|
|
|
ContextPath string `json:"contextPath"`
|
|
|
|
Text string `json:"text"`
|
|
|
|
Value string `json:"value"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type responseData struct {
|
|
|
|
Data json.RawMessage `json:"data"`
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
|
|
Limit uint `json:"limit"`
|
|
|
|
Page uint `json:"page"`
|
|
|
|
TotalPages uint `json:"totalPages"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *record) nativeToRecord(domain string) *models.RecordConfig {
|
|
|
|
// normalize cname,mx,ns records with dots to be consistent with our config format.
|
|
|
|
if r.Type == "ALIAS" || r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
|
|
|
|
if r.Content != "." {
|
|
|
|
r.Content = r.Content + "."
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
rc := &models.RecordConfig{
|
|
|
|
Type: "",
|
|
|
|
TTL: r.TTL,
|
|
|
|
MxPreference: r.Priority,
|
|
|
|
SrvPriority: r.Priority,
|
|
|
|
Original: r,
|
|
|
|
}
|
|
|
|
rc.SetLabelFromFQDN(r.Name, domain)
|
|
|
|
|
|
|
|
var err error
|
|
|
|
switch r.Type {
|
|
|
|
case "ALIAS":
|
|
|
|
rc.Type = r.Type
|
|
|
|
rc.SetTarget(r.Content)
|
|
|
|
case "NULLMX":
|
|
|
|
err = rc.PopulateFromString("MX", "0 .", domain)
|
|
|
|
case "MX":
|
|
|
|
err = rc.SetTargetMX(uint16(r.Priority), r.Content)
|
2023-01-17 23:57:20 +08:00
|
|
|
case "PTR":
|
|
|
|
rc.Type = r.Type
|
|
|
|
err = rc.SetTarget(r.Content + ".")
|
2021-03-09 08:25:55 +08:00
|
|
|
case "SRV":
|
|
|
|
err = rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Content)
|
|
|
|
default:
|
|
|
|
if err := rc.PopulateFromString(r.Type, r.Content, domain); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rc
|
|
|
|
}
|
|
|
|
|
|
|
|
func recordToNative(rc *models.RecordConfig) *record {
|
|
|
|
record := &record{
|
|
|
|
Name: rc.NameFQDN,
|
|
|
|
Type: rc.Type,
|
|
|
|
Content: strings.TrimSuffix(rc.GetTargetCombined(), "."),
|
|
|
|
TTL: rc.TTL,
|
|
|
|
}
|
|
|
|
|
|
|
|
switch rc.Type { // #rtype_variations
|
|
|
|
case "A", "AAAA", "ALIAS", "CAA", "CNAME", "DNSKEY", "DS", "NS", "NSEC", "NSEC3", "NSEC3PARAM", "PTR", "RRSIG", "SSHFP", "TSLA":
|
|
|
|
// Nothing special.
|
|
|
|
case "TXT":
|
2023-01-17 23:57:58 +08:00
|
|
|
txtStrings := make([]string, len(rc.TxtStrings))
|
|
|
|
copy(txtStrings, rc.TxtStrings)
|
|
|
|
|
|
|
|
// Escape quotes
|
|
|
|
for i := range txtStrings {
|
|
|
|
txtStrings[i] = fmt.Sprintf(`"%s"`, strings.ReplaceAll(txtStrings[i], `"`, `\"`))
|
2021-03-09 08:25:55 +08:00
|
|
|
}
|
2023-01-17 23:57:58 +08:00
|
|
|
|
|
|
|
record.Content = strings.Join(txtStrings, " ")
|
2021-03-09 08:25:55 +08:00
|
|
|
case "MX":
|
|
|
|
record.Priority = rc.MxPreference
|
|
|
|
record.Content = strings.TrimSuffix(rc.GetTargetField(), ".")
|
|
|
|
if record.Content == "" {
|
|
|
|
record.Type = "NULLMX"
|
|
|
|
record.Priority = 10
|
|
|
|
}
|
|
|
|
case "SRV":
|
|
|
|
record.Priority = rc.SrvPriority
|
|
|
|
record.Content = fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, strings.TrimSuffix(rc.GetTargetField(), "."))
|
|
|
|
default:
|
|
|
|
log.Printf("hosting.de rtype %v unimplemented", rc.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
return record
|
|
|
|
}
|