package server import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // PorkbunDNS manages TXT records through Porkbun's DNS API // (https://api.porkbun.com/api/json/v3). type PorkbunDNS struct { APIKey string SecretKey string Zone string BaseURL string // default https://api.porkbun.com/api/json/v3 HTTP *http.Client } const porkbunDefaultBaseURL = "https://api.porkbun.com/api/json/v3" func (p *PorkbunDNS) base() string { if p.BaseURL != "" { return p.BaseURL } return porkbunDefaultBaseURL } func (p *PorkbunDNS) client() *http.Client { if p.HTTP != nil { return p.HTTP } return &http.Client{Timeout: 20 * time.Second} } func (p *PorkbunDNS) post(ctx context.Context, path string, body map[string]string) error { body["apikey"] = p.APIKey body["secretapikey"] = p.SecretKey raw, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.base()+path, bytes.NewReader(raw)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := p.client().Do(req) if err != nil { return err } defer resp.Body.Close() data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("porkbun %s: HTTP %d: %s", path, resp.StatusCode, data) } var result struct { Status string `json:"status"` Message string `json:"message"` } if err := json.Unmarshal(data, &result); err != nil { return fmt.Errorf("porkbun %s: unreadable response: %w", path, err) } if result.Status != "SUCCESS" { return fmt.Errorf("porkbun %s: %s: %s", path, result.Status, result.Message) } return nil } func (p *PorkbunDNS) CreateTXT(ctx context.Context, name, value string) error { relative, err := relativeRecordName(name, p.Zone) if err != nil { return err } return p.post(ctx, "/dns/create/"+p.Zone, map[string]string{ "name": relative, "type": "TXT", "content": value, "ttl": "600", }) } func (p *PorkbunDNS) DeleteTXT(ctx context.Context, name string) error { relative, err := relativeRecordName(name, p.Zone) if err != nil { return err } return p.post(ctx, "/dns/deleteByNameType/"+p.Zone+"/TXT/"+relative, map[string]string{}) }