1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00
Files
stackexchange-dnscontrol/providers/ns1/ns1provider.go
Tom Limoncelli 8dea9edc34 Re-engineer TXT records for simplicity and better compliance (#1063)
TXT records are now handled different.

1. The raw input from dnsconfig.js is passed all the way to the provider. The provider can determine if it can or can't handle such records (auditrecords.go) and processes them internally as such.
2. The CanUseTXTMulti capability is no longer needed.

* DSPs now register a table of functions
* Use audits for txt record variations
* unit tests pass. integration fails.
* fix deepcopy problem
* rename to AuditRecordSupport
* Reduce use of TXTMulti
* Remove CanUseTXTMulti
* fix Test Skip
* fix DO
* fix vultr
* fix NDC
* msdns fixes
* Fix powerdns and cloudflare
* HEDNS: Fix usage of target field to resolve TXT handling (#1067)
* Fix HEXONET

Co-authored-by: Robert Blenkinsopp <robert@blenkinsopp.net>
Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
2021-03-07 13:19:22 -05:00

182 lines
5.2 KiB
Go

package ns1
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
"gopkg.in/ns1/ns1-go.v2/rest/model/filter"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/providers"
)
var docNotes = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.DocCreateDomains: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.DocDualHost: providers.Can(),
}
func init() {
fns := providers.DspFuncs{
Initializer: newProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NS1", fns, providers.CanUseSRV, docNotes)
providers.RegisterCustomRecordType("NS1_URLFWD", "NS1", "URLFWD")
}
type nsone struct {
*rest.Client
}
func newProvider(creds map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
if creds["api_token"] == "" {
return nil, fmt.Errorf("api_token required for ns1")
}
return &nsone{rest.NewClient(http.DefaultClient, rest.SetAPIKey(creds["api_token"]))}, nil
}
func (n *nsone) GetNameservers(domain string) ([]*models.Nameserver, error) {
z, _, err := n.Zones.Get(domain)
if err != nil {
return nil, err
}
return models.ToNameservers(z.DNSServers)
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (n *nsone) GetZoneRecords(domain string) (models.Records, error) {
return nil, fmt.Errorf("not implemented")
// This enables the get-zones subcommand.
// Implement this by extracting the code from GetDomainCorrections into
// a single function. For most providers this should be relatively easy.
}
func (n *nsone) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
//dc.CombineMXs()
z, _, err := n.Zones.Get(dc.Name)
if err != nil {
return nil, err
}
found := models.Records{}
for _, r := range z.Records {
zrs, err := convert(r, dc.Name)
if err != nil {
return nil, err
}
found = append(found, zrs...)
}
foundGrouped := found.GroupedByKey()
desiredGrouped := dc.Records.GroupedByKey()
// Normalize
models.PostProcessRecords(found)
differ := diff.New(dc)
changedGroups, err := differ.ChangedGroups(found)
if err != nil {
return nil, err
}
corrections := []*models.Correction{}
// each name/type is given to the api as a unit.
for k, descs := range changedGroups {
key := k
desc := strings.Join(descs, "\n")
_, current := foundGrouped[k]
recs, wanted := desiredGrouped[k]
if wanted && !current {
// pure addition
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error { return n.add(recs, dc.Name) },
})
} else if current && !wanted {
// pure deletion
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error { return n.remove(key, dc.Name) },
})
} else {
// modification
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error { return n.modify(recs, dc.Name) },
})
}
}
return corrections, nil
}
func (n *nsone) add(recs models.Records, domain string) error {
_, err := n.Records.Create(buildRecord(recs, domain, ""))
return err
}
func (n *nsone) remove(key models.RecordKey, domain string) error {
_, err := n.Records.Delete(domain, key.NameFQDN, key.Type)
return err
}
func (n *nsone) modify(recs models.Records, domain string) error {
_, err := n.Records.Update(buildRecord(recs, domain, ""))
return err
}
func buildRecord(recs models.Records, domain string, id string) *dns.Record {
r := recs[0]
rec := &dns.Record{
Domain: r.GetLabelFQDN(),
Type: r.Type,
ID: id,
TTL: int(r.TTL),
Zone: domain,
Filters: []*filter.Filter{}, // Work through a bug in the NS1 API library that causes 400 Input validation failed (Value None for field '<obj>.filters' is not of type array)
}
for _, r := range recs {
if r.Type == "MX" {
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(fmt.Sprintf("%d %v", r.MxPreference, r.GetTargetField()), " ")})
} else if r.Type == "TXT" {
rec.AddAnswer(&dns.Answer{Rdata: r.TxtStrings})
} else if r.Type == "SRV" {
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(fmt.Sprintf("%d %d %d %v", r.SrvPriority, r.SrvWeight, r.SrvPort, r.GetTargetField()), " ")})
} else {
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(r.GetTargetField(), " ")})
}
}
return rec
}
func convert(zr *dns.ZoneRecord, domain string) ([]*models.RecordConfig, error) {
found := []*models.RecordConfig{}
for _, ans := range zr.ShortAns {
rec := &models.RecordConfig{
TTL: uint32(zr.TTL),
Original: zr,
}
rec.SetLabelFromFQDN(zr.Domain, domain)
switch rtype := zr.Type; rtype {
case "ALIAS":
case "URLFWD":
rec.Type = rtype
if err := rec.SetTarget(ans); err != nil {
panic(fmt.Errorf("unparsable %s record received from ns1: %w", rtype, err))
}
default:
if err := rec.PopulateFromString(rtype, ans, domain); err != nil {
panic(fmt.Errorf("unparsable record received from ns1: %w", err))
}
}
found = append(found, rec)
}
return found, nil
}