mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
NEW PROVIDER: Packetframe (#1347)
* Implement Packetframe provider * Packetframe: Move to authorization and fix multiple TXT records * AKAMAIEDGEDNS: fix staticcheck warnings/errors (#1346) * downcase TLSA * Akamai provider * Akamai provider * EdgeDNS provider * AkamaiEdgeDNS provider * AkamaiEdgeDNS provider * AkamaiEdgeDNS provider * AKAMAIEDGEDNS: fix staticcheck warnings/errors Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com> * PacketframeProvider minor cleanup * Packetframe v4 moved to production * Packetframe Provider: Finish the rest of provider steps * Packetframe: Make stylistic changes, update nameservers, apikey -> token Co-authored-by: Steven Vernick <78868407+svernick@users.noreply.github.com> Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
186
providers/packetframe/api.go
Normal file
186
providers/packetframe/api.go
Normal file
@ -0,0 +1,186 @@
|
||||
package packetframe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
mediaType = "application/json"
|
||||
defaultBaseURL = "https://packetframe.com/api/"
|
||||
)
|
||||
|
||||
type zone struct {
|
||||
ID string `json:"id"`
|
||||
Zone string `json:"zone"`
|
||||
Users []string `json:"users"`
|
||||
UserEmails []string `json:"user_emails"`
|
||||
}
|
||||
|
||||
type domainResponse struct {
|
||||
Data struct {
|
||||
Zones []zone `json:"zones"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type deleteRequest struct {
|
||||
Record string `json:"record"`
|
||||
Zone string `json:"zone"`
|
||||
}
|
||||
|
||||
type recordResponse struct {
|
||||
Data struct {
|
||||
Records []domainRecord `json:"records"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type domainRecord struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
TTL int `json:"ttl"`
|
||||
Proxy bool `json:"proxy"`
|
||||
Zone string `json:"zone"`
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) fetchDomainList() error {
|
||||
c.domainIndex = map[string]zone{}
|
||||
dr := &domainResponse{}
|
||||
endpoint := "dns/zones"
|
||||
if err := c.get(endpoint, dr); err != nil {
|
||||
return fmt.Errorf("failed fetching domain list (Packetframe): %w", err)
|
||||
}
|
||||
for _, zone := range dr.Data.Zones {
|
||||
c.domainIndex[zone.Zone] = zone
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) getRecords(zoneID string) ([]domainRecord, error) {
|
||||
var records []domainRecord
|
||||
dr := &recordResponse{}
|
||||
endpoint := "dns/records/" + zoneID
|
||||
if err := c.get(endpoint, dr); err != nil {
|
||||
return records, fmt.Errorf("failed fetching domain list (Packetframe): %w", err)
|
||||
}
|
||||
records = append(records, dr.Data.Records...)
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) createRecord(rec *domainRecord) (*domainRecord, error) {
|
||||
endpoint := "dns/records"
|
||||
|
||||
req, err := c.newRequest(http.MethodPost, endpoint, rec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) modifyRecord(rec *domainRecord) error {
|
||||
endpoint := "dns/records"
|
||||
|
||||
req, err := c.newRequest(http.MethodPut, endpoint, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) deleteRecord(zoneID string, recordID string) error {
|
||||
endpoint := "dns/records"
|
||||
req, err := c.newRequest(http.MethodDelete, endpoint, deleteRequest{Zone: zoneID, Record: recordID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.handleErrors(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) newRequest(method, endpoint string, body interface{}) (*http.Request, error) {
|
||||
rel, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := c.baseURL.ResolveReference(rel)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", mediaType)
|
||||
req.Header.Add("Accept", mediaType)
|
||||
req.Header.Add("Authorization", "Token "+c.token)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) get(endpoint string, target interface{}) error {
|
||||
req, err := c.newRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.handleErrors(resp)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
return decoder.Decode(target)
|
||||
}
|
||||
|
||||
func (c *packetframeProvider) handleErrors(resp *http.Response) error {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dr := &domainResponse{}
|
||||
json.Unmarshal(body, &dr)
|
||||
|
||||
return fmt.Errorf("packetframe API error: %s", dr.Message)
|
||||
}
|
11
providers/packetframe/auditrecords.go
Normal file
11
providers/packetframe/auditrecords.go
Normal file
@ -0,0 +1,11 @@
|
||||
package packetframe
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
)
|
||||
|
||||
// AuditRecords returns an error if any records are not
|
||||
// supportable by this provider.
|
||||
func AuditRecords(records []*models.RecordConfig) error {
|
||||
return nil
|
||||
}
|
237
providers/packetframe/packetframeProvider.go
Normal file
237
providers/packetframe/packetframeProvider.go
Normal file
@ -0,0 +1,237 @@
|
||||
package packetframe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
)
|
||||
|
||||
// packetframeProvider is the handle for this provider.
|
||||
type packetframeProvider struct {
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
token string
|
||||
domainIndex map[string]zone
|
||||
}
|
||||
|
||||
var defaultNameServerNames = []string{
|
||||
"ns1.packetframe.com",
|
||||
"ns2.packetframe.com",
|
||||
}
|
||||
|
||||
// newPacketframe creates the provider.
|
||||
func newPacketframe(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
if m["token"] == "" {
|
||||
return nil, fmt.Errorf("missing Packetframe token")
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(defaultBaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base URL for Packetframe")
|
||||
}
|
||||
client := http.Client{}
|
||||
|
||||
api := &packetframeProvider{client: &client, baseURL: baseURL, token: m["token"]}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
providers.DocDualHost: providers.Cannot(),
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
providers.CanUseSRV: providers.Can(),
|
||||
providers.CanUsePTR: providers.Can(),
|
||||
providers.CanGetZones: providers.Unimplemented(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: newPacketframe,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType("PACKETFRAME", fns, features)
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
func (api *packetframeProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
return models.ToNameservers(defaultNameServerNames)
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (api *packetframeProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||
|
||||
if api.domainIndex == nil {
|
||||
if err := api.fetchDomainList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
zone, ok := api.domainIndex[domain+"."]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q not a zone in Packetframe account", domain)
|
||||
}
|
||||
|
||||
records, err := api.getRecords(zone.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load records for domain %q", domain)
|
||||
}
|
||||
|
||||
existingRecords := make([]*models.RecordConfig, len(records))
|
||||
|
||||
dc := models.DomainConfig{
|
||||
Name: domain,
|
||||
}
|
||||
|
||||
for i := range records {
|
||||
existingRecords[i] = toRc(&dc, &records[i])
|
||||
}
|
||||
|
||||
return existingRecords, nil
|
||||
}
|
||||
|
||||
// GetDomainCorrections returns the corrections for a domain.
|
||||
func (api *packetframeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc, err := dc.Copy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dc.Punycode()
|
||||
|
||||
if api.domainIndex == nil {
|
||||
if err := api.fetchDomainList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
zone, ok := api.domainIndex[dc.Name+"."]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no such zone %q in Packetframe account", dc.Name)
|
||||
}
|
||||
|
||||
records, err := api.getRecords(zone.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load records for domain %q", dc.Name)
|
||||
}
|
||||
|
||||
existingRecords := make([]*models.RecordConfig, len(records))
|
||||
|
||||
for i := range records {
|
||||
existingRecords[i] = toRc(dc, &records[i])
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.PostProcessRecords(existingRecords)
|
||||
|
||||
differ := diff.New(dc)
|
||||
_, create, delete, modify, err := differ.IncrementalDiff(existingRecords)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var corrections []*models.Correction
|
||||
|
||||
for _, m := range create {
|
||||
req, err := toReq(zone.ID, dc, m.Desired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
corr := &models.Correction{
|
||||
Msg: m.String(),
|
||||
F: func() error {
|
||||
_, err := api.createRecord(req)
|
||||
return err
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
for _, m := range delete {
|
||||
original := m.Existing.Original.(*domainRecord)
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("Deleting record %q from %q", original.ID, zone.Zone),
|
||||
F: func() error {
|
||||
err := api.deleteRecord(zone.ID, original.ID)
|
||||
return err
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
for _, m := range modify {
|
||||
original := m.Existing.Original.(*domainRecord)
|
||||
req, _ := toReq(zone.ID, dc, m.Desired)
|
||||
req.ID = original.ID
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("Modifying record %q from %q", original.ID, zone.Zone),
|
||||
F: func() error {
|
||||
err := api.modifyRecord(req)
|
||||
return err
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
func toReq(zoneID string, dc *models.DomainConfig, rc *models.RecordConfig) (*domainRecord, error) {
|
||||
req := &domainRecord{
|
||||
Type: rc.Type,
|
||||
TTL: int(rc.TTL),
|
||||
Label: rc.GetLabel(),
|
||||
Zone: zoneID,
|
||||
}
|
||||
|
||||
switch rc.Type { // #rtype_variations
|
||||
case "A", "AAAA", "PTR", "TXT", "CNAME", "NS":
|
||||
req.Value = rc.GetTargetField()
|
||||
case "MX":
|
||||
req.Value = fmt.Sprintf("%d %s", rc.MxPreference, rc.GetTargetField())
|
||||
case "SRV":
|
||||
req.Value = fmt.Sprintf("%d %d %d %s", rc.SrvPriority, rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
|
||||
default:
|
||||
return nil, fmt.Errorf("packetframe.toReq rtype %q unimplemented", rc.Type)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
Type: r.Type,
|
||||
TTL: uint32(r.TTL),
|
||||
Original: r,
|
||||
}
|
||||
|
||||
label := strings.TrimSuffix(r.Label, dc.Name+".")
|
||||
label = strings.TrimSuffix(label, ".")
|
||||
if label == "" {
|
||||
label = "@"
|
||||
}
|
||||
rc.SetLabel(label, dc.Name)
|
||||
|
||||
switch rtype := r.Type; rtype { // #rtype_variations
|
||||
case "TXT":
|
||||
rc.SetTargetTXTString(r.Value)
|
||||
case "SRV":
|
||||
spl := strings.Split(r.Value, " ")
|
||||
prio, _ := strconv.ParseUint(spl[0], 10, 16)
|
||||
weight, _ := strconv.ParseUint(spl[1], 10, 16)
|
||||
port, _ := strconv.ParseUint(spl[2], 10, 16)
|
||||
rc.SetTargetSRV(uint16(prio), uint16(weight), uint16(port), spl[3])
|
||||
case "MX":
|
||||
spl := strings.Split(r.Value, " ")
|
||||
prio, _ := strconv.ParseUint(spl[0], 10, 16)
|
||||
rc.SetTargetMX(uint16(prio), spl[1])
|
||||
default:
|
||||
rc.SetTarget(r.Value)
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
Reference in New Issue
Block a user