mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-10-06 11:56:50 +08:00
PORKBUN: support URL Forward (#3064)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
dd2030e2cb
commit
04f34cf2e3
7 changed files with 206 additions and 5 deletions
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
name: PORKBUN_URLFWD
|
||||||
|
parameters:
|
||||||
|
- name
|
||||||
|
- target
|
||||||
|
- modifiers...
|
||||||
|
provider: PORKBUN
|
||||||
|
parameter_types:
|
||||||
|
name: string
|
||||||
|
target: string
|
||||||
|
"modifiers...": RecordModifier[]
|
||||||
|
---
|
||||||
|
|
||||||
|
`PORKBUN_URLFWD` is a Porkbun-specific feature that maps to Porkbun's URL forwarding feature, which creates HTTP 301 (permanent) or 302 (temporary) redirects.
|
||||||
|
|
||||||
|
|
||||||
|
{% code title="dnsconfig.js" %}
|
||||||
|
```javascript
|
||||||
|
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
|
||||||
|
PORKBUN_URLFWD("urlfwd1", "http://example.com"),
|
||||||
|
PORKBUN_URLFWD("urlfwd2", "http://example.org", {type: "permanent", includePath: "yes", wildcard: "no"})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
The fields are:
|
||||||
|
* name: the record name
|
||||||
|
* target: where you'd like to forward the domain to
|
||||||
|
* type: valid types are: `temporary` (302 / 307) or `permanent` (301), default to `temporary`
|
||||||
|
* includePath: whether to include the URI path in the redirection. Valid options are `yes` or `no`, default to `no`
|
||||||
|
* wildcard: forward all subdomains of the domain. Valid options are `yes` or `no`, default to `yes`
|
|
@ -764,6 +764,15 @@ func ns1Urlfwd(name, target string) *models.RecordConfig {
|
||||||
return makeRec(name, target, "NS1_URLFWD")
|
return makeRec(name, target, "NS1_URLFWD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func porkbunUrlfwd(name, target, t, includePath, wildcard string) *models.RecordConfig {
|
||||||
|
r := makeRec(name, target, "PORKBUN_URLFWD")
|
||||||
|
r.Metadata = make(map[string]string)
|
||||||
|
r.Metadata["type"] = t
|
||||||
|
r.Metadata["includePath"] = includePath
|
||||||
|
r.Metadata["wildcard"] = wildcard
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func clear() *TestCase {
|
func clear() *TestCase {
|
||||||
return tc("Empty")
|
return tc("Empty")
|
||||||
}
|
}
|
||||||
|
@ -2280,6 +2289,15 @@ func makeTests() []*TestGroup {
|
||||||
ovhdmarc("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")),
|
ovhdmarc("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// PORKBUN features
|
||||||
|
|
||||||
|
testgroup("PORKBUN_URLFWD tests",
|
||||||
|
only("PORKBUN"),
|
||||||
|
tc("Add a urlfwd", porkbunUrlfwd("urlfwd1", "http://example.com", "", "", "")),
|
||||||
|
tc("Update a urlfwd", porkbunUrlfwd("urlfwd1", "http://example.org", "", "", "")),
|
||||||
|
tc("Update a urlfwd with metadata", porkbunUrlfwd("urlfwd1", "http://example.org", "permanent", "no", "no")),
|
||||||
|
),
|
||||||
|
|
||||||
// This MUST be the last test.
|
// This MUST be the last test.
|
||||||
testgroup("final",
|
testgroup("final",
|
||||||
tc("final", txt("final", `TestDNSProviders was successful!`)),
|
tc("final", txt("final", `TestDNSProviders was successful!`)),
|
||||||
|
|
|
@ -123,7 +123,7 @@ func (dc *DomainConfig) Punycode() error {
|
||||||
|
|
||||||
// Set the target:
|
// Set the target:
|
||||||
switch rec.Type { // #rtype_variations
|
switch rec.Type { // #rtype_variations
|
||||||
case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "NS1_URLFWD", "AKAMAICDN", "CLOUDNS_WR":
|
case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "NS1_URLFWD", "AKAMAICDN", "CLOUDNS_WR", "PORKBUN_URLFWD":
|
||||||
// These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record)
|
// These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record)
|
||||||
t, err := idna.ToASCII(rec.GetTargetField())
|
t, err := idna.ToASCII(rec.GetTargetField())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -47,6 +47,7 @@ import (
|
||||||
// NO_PURGE
|
// NO_PURGE
|
||||||
// NS1_URLFWD
|
// NS1_URLFWD
|
||||||
// PAGE_RULE
|
// PAGE_RULE
|
||||||
|
// PORKBUN_URLFWD
|
||||||
// PURGE
|
// PURGE
|
||||||
// URL
|
// URL
|
||||||
// URL301
|
// URL301
|
||||||
|
|
|
@ -1260,6 +1260,7 @@ var URL301 = recordBuilder('URL301');
|
||||||
var FRAME = recordBuilder('FRAME');
|
var FRAME = recordBuilder('FRAME');
|
||||||
var NS1_URLFWD = recordBuilder('NS1_URLFWD');
|
var NS1_URLFWD = recordBuilder('NS1_URLFWD');
|
||||||
var CLOUDNS_WR = recordBuilder('CLOUDNS_WR');
|
var CLOUDNS_WR = recordBuilder('CLOUDNS_WR');
|
||||||
|
var PORKBUN_URLFWD = recordBuilder('PORKBUN_URLFWD');
|
||||||
|
|
||||||
// LOC_BUILDER_DD takes an object:
|
// LOC_BUILDER_DD takes an object:
|
||||||
// label: The DNS label for the LOC record. (default: '@')
|
// label: The DNS label for the LOC record. (default: '@')
|
||||||
|
|
|
@ -36,10 +36,16 @@ type domainRecord struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
TTL string `json:"ttl"`
|
TTL string `json:"ttl"`
|
||||||
Prio string `json:"prio"`
|
Prio string `json:"prio"`
|
||||||
|
// Forwarding
|
||||||
|
Subdomain string `json:"subdomain"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
IncludePath string `json:"includePath"`
|
||||||
|
Wildcard string `json:"wildcard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordResponse struct {
|
type recordResponse struct {
|
||||||
Records []domainRecord `json:"records"`
|
Records []domainRecord `json:"records"`
|
||||||
|
Forwards []domainRecord `json:"forwards"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type domainListRecord struct {
|
type domainListRecord struct {
|
||||||
|
@ -146,6 +152,51 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *porkbunProvider) createUrlForwardingRecord(domain string, rec requestParams) error {
|
||||||
|
if _, err := c.post("/domain/addUrlForward/"+domain, rec); err != nil {
|
||||||
|
return fmt.Errorf("failed create url forwarding record (porkbun): %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *porkbunProvider) deleteUrlForwardingRecord(domain string, recordID string) error {
|
||||||
|
params := requestParams{}
|
||||||
|
if _, err := c.post(fmt.Sprintf("/domain/deleteUrlForward/%s/%s", domain, recordID), params); err != nil {
|
||||||
|
return fmt.Errorf("failed delete url forwarding record (porkbun): %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *porkbunProvider) modifyUrlForwardingRecord(domain string, recordID string, rec requestParams) error {
|
||||||
|
if err := c.deleteUrlForwardingRecord(domain, recordID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.createUrlForwardingRecord(domain, rec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *porkbunProvider) getUrlForwardingRecords(domain string) ([]domainRecord, error) {
|
||||||
|
params := requestParams{}
|
||||||
|
var bodyString, err = c.post("/domain/getUrlForwarding/"+domain, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed fetching url forwarding record list from porkbun: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dr recordResponse
|
||||||
|
err = json.Unmarshal(bodyString, &dr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed parsing url forwarding record list from porkbun: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []domainRecord
|
||||||
|
for _, rec := range dr.Forwards {
|
||||||
|
records = append(records, rec)
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
|
func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
|
||||||
params := requestParams{}
|
params := requestParams{}
|
||||||
var bodyString, err = c.post(fmt.Sprintf("/domain/getNs/%s", domain), params)
|
var bodyString, err = c.post(fmt.Sprintf("/domain/getNs/%s", domain), params)
|
||||||
|
|
|
@ -11,12 +11,20 @@ import (
|
||||||
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||||||
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||||
"github.com/StackExchange/dnscontrol/v4/providers"
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||||
|
|
||||||
|
"github.com/miekg/dns/dnsutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minimumTTL = 600
|
minimumTTL = 600
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
metaType = "type"
|
||||||
|
metaIncludePath = "includePath"
|
||||||
|
metaWildcard = "wildcard"
|
||||||
|
)
|
||||||
|
|
||||||
// https://kb.porkbun.com/article/63-how-to-switch-to-porkbuns-nameservers
|
// https://kb.porkbun.com/article/63-how-to-switch-to-porkbuns-nameservers
|
||||||
var defaultNS = []string{
|
var defaultNS = []string{
|
||||||
"curitiba.ns.porkbun.com",
|
"curitiba.ns.porkbun.com",
|
||||||
|
@ -78,6 +86,7 @@ func init() {
|
||||||
}
|
}
|
||||||
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
||||||
providers.RegisterMaintainer(providerName, providerMaintainer)
|
providers.RegisterMaintainer(providerName, providerMaintainer)
|
||||||
|
providers.RegisterCustomRecordType("PORKBUN_URLFWD", providerName, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNameservers returns the nameservers for a domain.
|
// GetNameservers returns the nameservers for a domain.
|
||||||
|
@ -85,6 +94,13 @@ func (c *porkbunProvider) GetNameservers(domain string) ([]*models.Nameserver, e
|
||||||
return models.ToNameservers(defaultNS)
|
return models.ToNameservers(defaultNS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func genComparable(rec *models.RecordConfig) string {
|
||||||
|
if rec.Type == "PORKBUN_URLFWD" {
|
||||||
|
return fmt.Sprintf("type=%s includePath=%s wildcard=%s", rec.Metadata[metaType], rec.Metadata[metaIncludePath], rec.Metadata[metaWildcard])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
||||||
func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
|
func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
|
||||||
var corrections []*models.Correction
|
var corrections []*models.Correction
|
||||||
|
@ -95,9 +111,24 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
|
||||||
// Make sure TTL larger than the minimum TTL
|
// Make sure TTL larger than the minimum TTL
|
||||||
for _, record := range dc.Records {
|
for _, record := range dc.Records {
|
||||||
record.TTL = fixTTL(record.TTL)
|
record.TTL = fixTTL(record.TTL)
|
||||||
|
if record.Type == "PORKBUN_URLFWD" {
|
||||||
|
record.TTL = 0
|
||||||
|
if record.Metadata == nil {
|
||||||
|
record.Metadata = make(map[string]string)
|
||||||
|
}
|
||||||
|
if record.Metadata[metaType] == "" {
|
||||||
|
record.Metadata[metaType] = "temporary"
|
||||||
|
}
|
||||||
|
if record.Metadata[metaIncludePath] == "" {
|
||||||
|
record.Metadata[metaIncludePath] = "no"
|
||||||
|
}
|
||||||
|
if record.Metadata[metaWildcard] == "" {
|
||||||
|
record.Metadata[metaWildcard] = "yes"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changes, err := diff2.ByRecord(existingRecords, dc, nil)
|
changes, err := diff2.ByRecord(existingRecords, dc, genComparable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -114,6 +145,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
|
||||||
corr = &models.Correction{
|
corr = &models.Correction{
|
||||||
Msg: change.Msgs[0],
|
Msg: change.Msgs[0],
|
||||||
F: func() error {
|
F: func() error {
|
||||||
|
if change.New[0].Type == "PORKBUN_URLFWD" {
|
||||||
|
return c.createUrlForwardingRecord(dc.Name, req)
|
||||||
|
}
|
||||||
return c.createRecord(dc.Name, req)
|
return c.createRecord(dc.Name, req)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -126,6 +160,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
|
||||||
corr = &models.Correction{
|
corr = &models.Correction{
|
||||||
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
|
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
|
||||||
F: func() error {
|
F: func() error {
|
||||||
|
if change.New[0].Type == "PORKBUN_URLFWD" {
|
||||||
|
return c.modifyUrlForwardingRecord(dc.Name, id, req)
|
||||||
|
}
|
||||||
return c.modifyRecord(dc.Name, id, req)
|
return c.modifyRecord(dc.Name, id, req)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -134,6 +171,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
|
||||||
corr = &models.Correction{
|
corr = &models.Correction{
|
||||||
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
|
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
|
||||||
F: func() error {
|
F: func() error {
|
||||||
|
if change.Old[0].Type == "PORKBUN_URLFWD" {
|
||||||
|
return c.deleteUrlForwardingRecord(dc.Name, id)
|
||||||
|
}
|
||||||
return c.deleteRecord(dc.Name, id)
|
return c.deleteRecord(dc.Name, id)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -152,9 +192,54 @@ func (c *porkbunProvider) GetZoneRecords(domain string, meta map[string]string)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
existingRecords := make([]*models.RecordConfig, len(records))
|
forwards, err := c.getUrlForwardingRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
existingRecords := make([]*models.RecordConfig, 0)
|
||||||
for i := range records {
|
for i := range records {
|
||||||
existingRecords[i] = toRc(domain, &records[i])
|
shouldSkip := false
|
||||||
|
if strings.HasSuffix(records[i].Content, ".porkbun.com") {
|
||||||
|
name := dnsutil.TrimDomainName(records[i].Name, domain)
|
||||||
|
if name == "@" {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
if records[i].Type == "ALIAS" {
|
||||||
|
for _, forward := range forwards {
|
||||||
|
if name == forward.Subdomain {
|
||||||
|
shouldSkip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if records[i].Type == "CNAME" {
|
||||||
|
for _, forward := range forwards {
|
||||||
|
if name == "*."+forward.Subdomain {
|
||||||
|
shouldSkip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldSkip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingRecords = append(existingRecords, toRc(domain, &records[i]))
|
||||||
|
}
|
||||||
|
for i := range forwards {
|
||||||
|
r := &forwards[i]
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: "PORKBUN_URLFWD",
|
||||||
|
Original: r,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
metaType: r.Type,
|
||||||
|
metaIncludePath: r.IncludePath,
|
||||||
|
metaWildcard: r.Wildcard,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rc.SetLabel(r.Subdomain, domain)
|
||||||
|
rc.SetTarget(r.Location)
|
||||||
|
existingRecords = append(existingRecords, rc)
|
||||||
}
|
}
|
||||||
return existingRecords, nil
|
return existingRecords, nil
|
||||||
}
|
}
|
||||||
|
@ -219,6 +304,20 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig {
|
||||||
|
|
||||||
// toReq takes a RecordConfig and turns it into the native format used by the API.
|
// toReq takes a RecordConfig and turns it into the native format used by the API.
|
||||||
func toReq(rc *models.RecordConfig) (requestParams, error) {
|
func toReq(rc *models.RecordConfig) (requestParams, error) {
|
||||||
|
if rc.Type == "PORKBUN_URLFWD" {
|
||||||
|
subdomain := rc.GetLabel()
|
||||||
|
if subdomain == "@" {
|
||||||
|
subdomain = ""
|
||||||
|
}
|
||||||
|
return requestParams{
|
||||||
|
"subdomain": subdomain,
|
||||||
|
"location": rc.GetTargetField(),
|
||||||
|
"type": rc.Metadata[metaType],
|
||||||
|
"includePath": rc.Metadata[metaIncludePath],
|
||||||
|
"wildcard": rc.Metadata[metaWildcard],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
req := requestParams{
|
req := requestParams{
|
||||||
"type": rc.Type,
|
"type": rc.Type,
|
||||||
"name": rc.GetLabel(),
|
"name": rc.GetLabel(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue