mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-11-09 16:00:36 +08:00
253 lines
7 KiB
Go
253 lines
7 KiB
Go
package porkbun
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
|
"github.com/failsafe-go/failsafe-go"
|
|
"github.com/failsafe-go/failsafe-go/failsafehttp"
|
|
"github.com/failsafe-go/failsafe-go/retrypolicy"
|
|
)
|
|
|
|
const (
|
|
baseURL = "https://api.porkbun.com/api/json/v3"
|
|
)
|
|
|
|
type porkbunProvider struct {
|
|
apiKey string
|
|
secretKey string
|
|
}
|
|
|
|
type requestParams map[string]any
|
|
|
|
type errorResponse struct {
|
|
Status string `json:"status"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type domainRecord struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
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"`
|
|
Forwards []domainRecord `json:"forwards"`
|
|
}
|
|
|
|
type domainListRecord struct {
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
type domainListResponse struct {
|
|
Domains []domainListRecord `json:"domains"`
|
|
}
|
|
|
|
type nsResponse struct {
|
|
Nameservers []string `json:"ns"`
|
|
}
|
|
|
|
func (c *porkbunProvider) post(endpoint string, params requestParams) ([]byte, error) {
|
|
params["apikey"] = c.apiKey
|
|
params["secretapikey"] = c.secretKey
|
|
|
|
paramsJSON, err := json.Marshal(params)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
|
|
retryPolicy := failsafehttp.RetryPolicyBuilder().
|
|
WithMaxRetries(5).
|
|
// Exponential backoff between 1 and 10 seconds
|
|
WithBackoff(time.Second, 10*time.Second).
|
|
WithJitter(100 * time.Millisecond).
|
|
OnRetryScheduled(func(f failsafe.ExecutionScheduledEvent[*http.Response]) {
|
|
printer.Debugf("Porkbun API response code %d, waiting for %s until next attempt\n", f.LastResult().StatusCode, f.Delay)
|
|
}).
|
|
Build()
|
|
|
|
client := &http.Client{
|
|
Transport: failsafehttp.NewRoundTripper(nil, retryPolicy),
|
|
}
|
|
req, _ := http.NewRequest(http.MethodPost, baseURL+endpoint, bytes.NewBuffer(paramsJSON))
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
if errors.Is(err, retrypolicy.ErrExceeded) {
|
|
// Return the underlying error rather than the wrapped error, which has too much detail
|
|
return nil, retrypolicy.ErrExceeded
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
bodyString, _ := io.ReadAll(resp.Body)
|
|
|
|
// Got error from API ?
|
|
var errResp errorResponse
|
|
err = json.Unmarshal(bodyString, &errResp)
|
|
if err == nil {
|
|
if errResp.Status == "ERROR" {
|
|
return bodyString, fmt.Errorf("porkbun API error: %s URL:%s%s ", errResp.Message, req.Host, req.URL.RequestURI())
|
|
}
|
|
}
|
|
|
|
return bodyString, nil
|
|
}
|
|
|
|
func (c *porkbunProvider) createRecord(domain string, rec requestParams) error {
|
|
if _, err := c.post("/dns/create/"+domain, rec); err != nil {
|
|
return fmt.Errorf("failed create record (porkbun): %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *porkbunProvider) deleteRecord(domain string, recordID string) error {
|
|
params := requestParams{}
|
|
if _, err := c.post(fmt.Sprintf("/dns/delete/%s/%s", domain, recordID), params); err != nil {
|
|
return fmt.Errorf("failed delete record (porkbun): %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *porkbunProvider) modifyRecord(domain string, recordID string, rec requestParams) error {
|
|
if _, err := c.post(fmt.Sprintf("/dns/edit/%s/%s", domain, recordID), rec); err != nil {
|
|
return fmt.Errorf("failed update (porkbun): %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *porkbunProvider) getRecords(domain string) ([]domainRecord, error) {
|
|
params := requestParams{}
|
|
bodyString, err := c.post("/dns/retrieve/"+domain, params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed fetching record list from porkbun: %w", err)
|
|
}
|
|
|
|
var dr recordResponse
|
|
err = json.Unmarshal(bodyString, &dr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed parsing record list from porkbun: %w", err)
|
|
}
|
|
|
|
var records []domainRecord
|
|
for _, rec := range dr.Records {
|
|
if rec.Name == domain && rec.Type == "NS" {
|
|
continue
|
|
}
|
|
records = append(records, rec)
|
|
}
|
|
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{}
|
|
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)
|
|
}
|
|
|
|
return dr.Forwards, nil
|
|
}
|
|
|
|
func (c *porkbunProvider) getNameservers(domain string) ([]string, error) {
|
|
params := requestParams{}
|
|
bodyString, err := c.post("/domain/getNs/"+domain, params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed fetching nameserver list from porkbun: %w", err)
|
|
}
|
|
|
|
var ns nsResponse
|
|
err = json.Unmarshal(bodyString, &ns)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed parsing nameserver list from porkbun: %w", err)
|
|
}
|
|
|
|
sort.Strings(ns.Nameservers)
|
|
|
|
var nameservers []string
|
|
for _, nameserver := range ns.Nameservers {
|
|
// Remove the trailing dot only if it exists.
|
|
// This provider seems to add the trailing dot to some domains but not others.
|
|
// The .DE domains seem to always include the dot, others don't.
|
|
nameservers = append(nameservers, strings.TrimSuffix(nameserver, "."))
|
|
}
|
|
return nameservers, nil
|
|
}
|
|
|
|
func (c *porkbunProvider) updateNameservers(ns []string, domain string) error {
|
|
params := requestParams{}
|
|
params["ns"] = ns
|
|
if _, err := c.post("/domain/updateNs/"+domain, params); err != nil {
|
|
return fmt.Errorf("failed NS update (porkbun): %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *porkbunProvider) listAllDomains() ([]string, error) {
|
|
params := requestParams{}
|
|
bodyString, err := c.post("/domain/listAll", params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed listing all domains from porkbun: %w", err)
|
|
}
|
|
|
|
var dlr domainListResponse
|
|
err = json.Unmarshal(bodyString, &dlr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed parsing domain list from porkbun: %w", err)
|
|
}
|
|
|
|
var domains []string
|
|
for _, domain := range dlr.Domains {
|
|
domains = append(domains, domain.Domain)
|
|
}
|
|
sort.Strings(domains)
|
|
return domains, nil
|
|
}
|