mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 18:08:57 +08:00
b0f2945510
This should enable the diff2 code to be inserted with good "git blame" results for new code. I'm adding this early to catch any problems early.
288 lines
7.8 KiB
Go
288 lines
7.8 KiB
Go
package porkbun
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
|
)
|
|
|
|
const (
|
|
minimumTTL = 600
|
|
)
|
|
|
|
// https://kb.porkbun.com/article/63-how-to-switch-to-porkbuns-nameservers
|
|
var defaultNS = []string{
|
|
"curitiba.ns.porkbun.com",
|
|
"fortaleza.ns.porkbun.com",
|
|
"maceio.ns.porkbun.com",
|
|
"salvador.ns.porkbun.com",
|
|
}
|
|
|
|
// NewPorkbun creates the provider.
|
|
func NewPorkbun(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
c := &porkbunProvider{}
|
|
|
|
c.apiKey, c.secretKey = m["api_key"], m["secret_key"]
|
|
|
|
if c.apiKey == "" || c.secretKey == "" {
|
|
return nil, fmt.Errorf("missing porkbun api_key or secret_key")
|
|
}
|
|
|
|
// Validate authentication
|
|
if err := c.ping(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
providers.CanAutoDNSSEC: providers.Cannot(),
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanUseAlias: providers.Can(),
|
|
providers.CanUseCAA: providers.Unimplemented(), // CAA record for base domain is pinning to a fixed set once configure
|
|
providers.CanUseDS: providers.Cannot(),
|
|
providers.CanUseDSForChildren: providers.Cannot(),
|
|
providers.CanUseNAPTR: providers.Cannot(),
|
|
providers.CanUsePTR: providers.Cannot(),
|
|
providers.CanUseSOA: providers.Cannot(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CanUseSSHFP: providers.Cannot(),
|
|
providers.CanUseTLSA: providers.Can(),
|
|
providers.DocCreateDomains: providers.Cannot(),
|
|
providers.DocDualHost: providers.Cannot(),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
func init() {
|
|
fns := providers.DspFuncs{
|
|
Initializer: NewPorkbun,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType("PORKBUN", fns, features)
|
|
}
|
|
|
|
// GetNameservers returns the nameservers for a domain.
|
|
func (c *porkbunProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
return models.ToNameservers(defaultNS)
|
|
}
|
|
|
|
// GetDomainCorrections returns the corrections for a domain.
|
|
func (c *porkbunProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
dc, err := dc.Copy()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dc.Punycode()
|
|
|
|
existingRecords, err := c.GetZoneRecords(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Block changes to NS records for base domain
|
|
checkNSModifications(dc)
|
|
|
|
// Normalize
|
|
models.PostProcessRecords(existingRecords)
|
|
|
|
// Make sure TTL larger than the minimum TTL
|
|
for _, record := range dc.Records {
|
|
record.TTL = fixTTL(record.TTL)
|
|
}
|
|
|
|
var corrections []*models.Correction
|
|
if !diff2.EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
|
|
|
|
differ := diff.New(dc)
|
|
_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Deletes first so changing type works etc.
|
|
for _, m := range del {
|
|
id := m.Existing.Original.(*domainRecord).ID
|
|
corr := &models.Correction{
|
|
Msg: fmt.Sprintf("%s, porkbun ID: %s", m.String(), id),
|
|
F: func() error {
|
|
return c.deleteRecord(dc.Name, id)
|
|
},
|
|
}
|
|
corrections = append(corrections, corr)
|
|
}
|
|
|
|
for _, m := range create {
|
|
req, err := toReq(m.Desired)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
corr := &models.Correction{
|
|
Msg: m.String(),
|
|
F: func() error {
|
|
return c.createRecord(dc.Name, req)
|
|
},
|
|
}
|
|
corrections = append(corrections, corr)
|
|
}
|
|
|
|
for _, m := range modify {
|
|
id := m.Existing.Original.(*domainRecord).ID
|
|
req, err := toReq(m.Desired)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
corr := &models.Correction{
|
|
Msg: fmt.Sprintf("%s, porkbun ID: %s: ", m.String(), id),
|
|
F: func() error {
|
|
return c.modifyRecord(dc.Name, id, req)
|
|
},
|
|
}
|
|
corrections = append(corrections, corr)
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
// Insert Future diff2 version here.
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (c *porkbunProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|
records, err := c.getRecords(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
existingRecords := make([]*models.RecordConfig, len(records))
|
|
for i := range records {
|
|
existingRecords[i] = toRc(domain, &records[i])
|
|
}
|
|
return existingRecords, nil
|
|
}
|
|
|
|
// parses the porkbun format into our standard RecordConfig
|
|
func toRc(domain string, r *domainRecord) *models.RecordConfig {
|
|
ttl, _ := strconv.ParseUint(r.TTL, 10, 32)
|
|
priority, _ := strconv.ParseUint(r.Prio, 10, 16)
|
|
|
|
rc := &models.RecordConfig{
|
|
Type: r.Type,
|
|
TTL: uint32(ttl),
|
|
MxPreference: uint16(priority),
|
|
SrvPriority: uint16(priority),
|
|
Original: r,
|
|
}
|
|
rc.SetLabelFromFQDN(r.Name, domain)
|
|
|
|
switch rtype := r.Type; rtype { // #rtype_variations
|
|
case "TXT":
|
|
rc.SetTargetTXT(r.Content)
|
|
case "MX", "CNAME", "ALIAS", "NS":
|
|
if strings.HasSuffix(r.Content, ".") {
|
|
rc.SetTarget(r.Content)
|
|
} else {
|
|
rc.SetTarget(r.Content + ".")
|
|
}
|
|
case "CAA":
|
|
// 0, issue, "letsencrypt.org"
|
|
c := strings.Split(r.Content, " ")
|
|
|
|
caaFlag, _ := strconv.ParseUint(c[0], 10, 8)
|
|
rc.CaaFlag = uint8(caaFlag)
|
|
rc.CaaTag = c[1]
|
|
rc.SetTarget(strings.ReplaceAll(c[2], "\"", ""))
|
|
case "TLSA":
|
|
// 0 0 0 00000000000000000000000
|
|
c := strings.Split(r.Content, " ")
|
|
|
|
tlsaUsage, _ := strconv.ParseUint(c[0], 10, 8)
|
|
rc.TlsaUsage = uint8(tlsaUsage)
|
|
tlsaSelector, _ := strconv.ParseUint(c[1], 10, 8)
|
|
rc.TlsaSelector = uint8(tlsaSelector)
|
|
tlsaMatchingType, _ := strconv.ParseUint(c[2], 10, 8)
|
|
rc.TlsaMatchingType = uint8(tlsaMatchingType)
|
|
rc.SetTarget(c[3])
|
|
case "SRV":
|
|
// 5 5060 sip.example.com
|
|
c := strings.Split(r.Content, " ")
|
|
|
|
srvWeight, _ := strconv.ParseUint(c[0], 10, 16)
|
|
rc.SrvWeight = uint16(srvWeight)
|
|
srvPort, _ := strconv.ParseUint(c[1], 10, 16)
|
|
rc.SrvPort = uint16(srvPort)
|
|
rc.SetTarget(c[2])
|
|
default:
|
|
rc.SetTarget(r.Content)
|
|
}
|
|
|
|
return rc
|
|
}
|
|
|
|
// toReq takes a RecordConfig and turns it into the native format used by the API.
|
|
func toReq(rc *models.RecordConfig) (requestParams, error) {
|
|
req := requestParams{
|
|
"type": rc.Type,
|
|
"name": rc.GetLabel(),
|
|
"content": rc.GetTargetField(),
|
|
"ttl": strconv.Itoa(int(rc.TTL)),
|
|
}
|
|
|
|
// porkbun doesn't use "@", it uses an empty name
|
|
if req["name"] == "@" {
|
|
req["name"] = ""
|
|
}
|
|
|
|
switch rc.Type { // #rtype_variations
|
|
case "A", "AAAA", "NS", "ALIAS", "CNAME":
|
|
// Nothing special.
|
|
case "TXT":
|
|
req["content"] = rc.GetTargetTXTJoined()
|
|
case "MX":
|
|
req["prio"] = strconv.Itoa(int(rc.MxPreference))
|
|
case "SRV":
|
|
req["prio"] = strconv.Itoa(int(rc.SrvPriority))
|
|
req["content"] = fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
|
|
case "CAA":
|
|
req["content"] = fmt.Sprintf("%d %s \"%s\"", rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
|
case "TLSA":
|
|
req["content"] = fmt.Sprintf("%d %d %d %s",
|
|
rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.GetTargetField())
|
|
default:
|
|
return nil, fmt.Errorf("porkbun.toReq rtype %q unimplemented", rc.Type)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func checkNSModifications(dc *models.DomainConfig) {
|
|
newList := make([]*models.RecordConfig, 0, len(dc.Records))
|
|
for _, rec := range dc.Records {
|
|
if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name {
|
|
if strings.HasSuffix(rec.GetTargetField(), ".porkbun.com") {
|
|
printer.Warnf("porkbun does not support modifying NS records on base domain. %s will not be added.\n", rec.GetTargetField())
|
|
}
|
|
continue
|
|
}
|
|
newList = append(newList, rec)
|
|
}
|
|
dc.Records = newList
|
|
}
|
|
|
|
func fixTTL(ttl uint32) uint32 {
|
|
if ttl > minimumTTL {
|
|
return ttl
|
|
}
|
|
return minimumTTL
|
|
}
|