dnscontrol/providers/mythicbeasts/mythicbeastsProvider.go
tomf c9ce326ae1
MYTHICBEASTS: Add SSHFP record support (#2545)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
2023-09-05 11:27:04 -04:00

155 lines
5 KiB
Go

// Package mythicbeasts provides a provider for managing zones in Mythic Beasts.
//
// This package uses the Primary DNS API v2, as described in https://www.mythic-beasts.com/support/api/dnsv2
package mythicbeasts
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns"
)
// mythicBeastsDefaultNS lists the default nameservers, per https://www.mythic-beasts.com/support/domains/nameservers.
var mythicBeastsDefaultNS = []string{
"ns1.mythic-beasts.com",
"ns2.mythic-beasts.com",
}
// mythicBeastsProvider is the handle for this provider.
type mythicBeastsProvider struct {
secret string
keyID string
client *http.Client
}
var features = providers.DocumentationNotes{
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseLOC: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Cannot("Requires domain registered through Web UI"),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("MYTHICBEASTS", fns, features)
}
func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if conf["keyID"] == "" {
return nil, fmt.Errorf("missing Mythic Beasts auth keyID")
}
if conf["secret"] == "" {
return nil, fmt.Errorf("missing Mythic Beasts auth secret")
}
return &mythicBeastsProvider{
keyID: conf["keyID"],
secret: conf["secret"],
client: &http.Client{},
}, nil
}
func (n *mythicBeastsProvider) httpRequest(method, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, "https://api.mythic-beasts.com/dns/v2"+url, body)
if err != nil {
return nil, err
}
// https://www.mythic-beasts.com/support/api/auth
req.SetBasicAuth(n.keyID, n.secret)
req.Header.Add("Content-Type", "text/dns")
req.Header.Add("Accept", "text/dns")
return n.client.Do(req)
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (n *mythicBeastsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
resp, err := n.httpRequest("GET", "/zones/"+domain+"/records", nil)
if err != nil {
return nil, err
}
if got, want := resp.StatusCode, 200; got != want {
body, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body))
}
return zoneFileToRecords(resp.Body, domain)
}
func zoneFileToRecords(r io.Reader, origin string) (models.Records, error) {
zp := dns.NewZoneParser(r, origin, origin)
var records []*models.RecordConfig
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
rec, err := models.RRtoRC(rr, origin)
if err != nil {
return nil, err
}
records = append(records, &rec)
}
if err := zp.Err(); err != nil {
return nil, fmt.Errorf("parsing zone for %v: %w", origin, err)
}
return records, nil
}
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
msgs, changes, err := diff2.ByZone(actual, dc, nil)
if err != nil {
return nil, err
}
var corrections []*models.Correction
if changes {
corrections = append(corrections,
&models.Correction{
Msg: strings.Join(msgs, "\n"),
F: func() error {
var b strings.Builder
for _, record := range dc.Records {
switch rr := record.ToRR().(type) {
case *dns.SSHFP:
// "Hex strings [for SSHFP] must be in lower-case", per Mythic Beasts API docs.
// miekg's DNS outputs uppercase: https://github.com/miekg/dns/blob/48f38ebef989eedc6b57f1869ae849ccc8f5fe29/types.go#L988
fmt.Fprintf(&b, "%s %d %d %s\n", rr.Header().String(), rr.Algorithm, rr.Type, strings.ToLower(rr.FingerPrint))
default:
fmt.Fprintf(&b, "%v\n", rr.String())
}
}
resp, err := n.httpRequest("PUT", "/zones/"+dc.Name+"/records", strings.NewReader(b.String()))
if err != nil {
return err
}
if got, want := resp.StatusCode, 200; got != want {
body, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body))
}
return nil
},
})
}
return corrections, nil
}
// GetNameservers returns the nameservers for a domain.
func (n *mythicBeastsProvider) GetNameservers(domainName string) ([]*models.Nameserver, error) {
return models.ToNameservers(mythicBeastsDefaultNS)
}