PORKBUN: support URL Forward (#3064)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
imlonghao 2024-08-27 02:50:25 +08:00 committed by GitHub
parent dd2030e2cb
commit 04f34cf2e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 206 additions and 5 deletions

View file

@ -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`

View file

@ -764,6 +764,15 @@ func ns1Urlfwd(name, target string) *models.RecordConfig {
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 {
return tc("Empty")
}
@ -2280,6 +2289,15 @@ func makeTests() []*TestGroup {
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.
testgroup("final",
tc("final", txt("final", `TestDNSProviders was successful!`)),

View file

@ -123,7 +123,7 @@ func (dc *DomainConfig) Punycode() error {
// Set the target:
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)
t, err := idna.ToASCII(rec.GetTargetField())
if err != nil {

View file

@ -47,6 +47,7 @@ import (
// NO_PURGE
// NS1_URLFWD
// PAGE_RULE
// PORKBUN_URLFWD
// PURGE
// URL
// URL301

View file

@ -1260,6 +1260,7 @@ var URL301 = recordBuilder('URL301');
var FRAME = recordBuilder('FRAME');
var NS1_URLFWD = recordBuilder('NS1_URLFWD');
var CLOUDNS_WR = recordBuilder('CLOUDNS_WR');
var PORKBUN_URLFWD = recordBuilder('PORKBUN_URLFWD');
// LOC_BUILDER_DD takes an object:
// label: The DNS label for the LOC record. (default: '@')

View file

@ -36,10 +36,16 @@ type domainRecord struct {
Content string `json:"content"`
TTL string `json:"ttl"`
Prio string `json:"prio"`
// Forwarding
Subdomain string `json:"subdomain"`
Location string `json:"location"`
IncludePath string `json:"includePath"`
Wildcard string `json:"wildcard"`
}
type recordResponse struct {
Records []domainRecord `json:"records"`
Records []domainRecord `json:"records"`
Forwards []domainRecord `json:"forwards"`
}
type domainListRecord struct {
@ -146,6 +152,51 @@ func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
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) {
params := requestParams{}
var bodyString, err = c.post(fmt.Sprintf("/domain/getNs/%s", domain), params)

View file

@ -11,12 +11,20 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
"github.com/miekg/dns/dnsutil"
)
const (
minimumTTL = 600
)
const (
metaType = "type"
metaIncludePath = "includePath"
metaWildcard = "wildcard"
)
// https://kb.porkbun.com/article/63-how-to-switch-to-porkbuns-nameservers
var defaultNS = []string{
"curitiba.ns.porkbun.com",
@ -78,6 +86,7 @@ func init() {
}
providers.RegisterDomainServiceProviderType(providerName, fns, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
providers.RegisterCustomRecordType("PORKBUN_URLFWD", providerName, "")
}
// GetNameservers returns the nameservers for a domain.
@ -85,6 +94,13 @@ func (c *porkbunProvider) GetNameservers(domain string) ([]*models.Nameserver, e
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.
func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
var corrections []*models.Correction
@ -95,9 +111,24 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
// Make sure TTL larger than the minimum TTL
for _, record := range dc.Records {
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 {
return nil, err
}
@ -114,6 +145,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
corr = &models.Correction{
Msg: change.Msgs[0],
F: func() error {
if change.New[0].Type == "PORKBUN_URLFWD" {
return c.createUrlForwardingRecord(dc.Name, req)
}
return c.createRecord(dc.Name, req)
},
}
@ -126,6 +160,9 @@ func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi
corr = &models.Correction{
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
F: func() error {
if change.New[0].Type == "PORKBUN_URLFWD" {
return c.modifyUrlForwardingRecord(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{
Msg: fmt.Sprintf("%s, porkbun ID: %s", change.Msgs[0], id),
F: func() error {
if change.Old[0].Type == "PORKBUN_URLFWD" {
return c.deleteUrlForwardingRecord(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 {
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 {
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
}
@ -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.
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{
"type": rc.Type,
"name": rc.GetLabel(),