client.go raw

   1  // Package porkbun contains a client of the DNS API of Porkdun.
   2  package porkbun
   3  
   4  import (
   5  	"bytes"
   6  	"context"
   7  	"encoding/json"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"strconv"
  13  	"time"
  14  )
  15  
  16  const defaultBaseURL = "https://api.porkbun.com/api/json/v3/"
  17  
  18  const statusSuccess = "SUCCESS"
  19  
  20  // DefaultTTL The minimum and the default is 300 seconds.
  21  const DefaultTTL = "300"
  22  
  23  // Client an API client for Porkdun.
  24  type Client struct {
  25  	secretAPIKey string
  26  	apiKey       string
  27  
  28  	BaseURL    *url.URL
  29  	HTTPClient *http.Client
  30  }
  31  
  32  // New creates a new Client.
  33  func New(secretAPIKey, apiKey string) *Client {
  34  	baseURL, _ := url.Parse(defaultBaseURL)
  35  
  36  	return &Client{
  37  		secretAPIKey: secretAPIKey,
  38  		apiKey:       apiKey,
  39  		BaseURL:      baseURL,
  40  		HTTPClient:   &http.Client{Timeout: 10 * time.Second},
  41  	}
  42  }
  43  
  44  // Ping tests communication with the API.
  45  func (c *Client) Ping(ctx context.Context) (string, error) {
  46  	endpoint := c.BaseURL.JoinPath("ping")
  47  
  48  	respBody, err := c.do(ctx, endpoint, nil)
  49  	if err != nil {
  50  		return "", err
  51  	}
  52  
  53  	pingResp := pingResponse{}
  54  	err = json.Unmarshal(respBody, &pingResp)
  55  	if err != nil {
  56  		return "", fmt.Errorf("failed to unmarshal response: %w", err)
  57  	}
  58  
  59  	if pingResp.Status.Status != statusSuccess {
  60  		return "", pingResp.Status
  61  	}
  62  
  63  	return pingResp.YourIP, nil
  64  }
  65  
  66  // CreateRecord creates a DNS record.
  67  //
  68  //	name (optional): The subdomain for the record being created, not including the domain itself. Leave blank to create a record on the root domain. Use * to create a wildcard record.
  69  //	type: The type of record being created. Valid types are: A, MX, CNAME, ALIAS, TXT, NS, AAAA, SRV, TLSA, CAA
  70  //	content: The answer content for the record.
  71  //	ttl (optional): The time to live in seconds for the record. The minimum and the default is 300 seconds.
  72  //	prio (optional) The priority of the record for those that support it.
  73  func (c *Client) CreateRecord(ctx context.Context, domain string, record Record) (int, error) {
  74  	endpoint := c.BaseURL.JoinPath("dns", "create", domain)
  75  
  76  	respBody, err := c.do(ctx, endpoint, record)
  77  	if err != nil {
  78  		return 0, err
  79  	}
  80  
  81  	createResp := createResponse{}
  82  	err = json.Unmarshal(respBody, &createResp)
  83  	if err != nil {
  84  		return 0, fmt.Errorf("failed to unmarshal response: %w", err)
  85  	}
  86  
  87  	if createResp.Status.Status != statusSuccess {
  88  		return 0, createResp.Status
  89  	}
  90  
  91  	return createResp.ID, nil
  92  }
  93  
  94  // EditRecord edits a DNS record.
  95  //
  96  //	name (optional): The subdomain for the record being created, not including the domain itself. Leave blank to create a record on the root domain. Use * to create a wildcard record.
  97  //	type: The type of record being created. Valid types are: A, MX, CNAME, ALIAS, TXT, NS, AAAA, SRV, TLSA, CAA
  98  //	content: The answer content for the record.
  99  //	ttl (optional): The time to live in seconds for the record. The minimum and the default is 300 seconds.
 100  //	prio (optional) The priority of the record for those that support it.
 101  func (c *Client) EditRecord(ctx context.Context, domain string, id int, record Record) error {
 102  	endpoint := c.BaseURL.JoinPath("dns", "edit", domain, strconv.Itoa(id))
 103  
 104  	respBody, err := c.do(ctx, endpoint, record)
 105  	if err != nil {
 106  		return err
 107  	}
 108  
 109  	statusResp := Status{}
 110  	err = json.Unmarshal(respBody, &statusResp)
 111  	if err != nil {
 112  		return fmt.Errorf("failed to unmarshal response: %w", err)
 113  	}
 114  
 115  	if statusResp.Status != statusSuccess {
 116  		return statusResp
 117  	}
 118  
 119  	return nil
 120  }
 121  
 122  // DeleteRecord deletes a specific DNS record.
 123  func (c *Client) DeleteRecord(ctx context.Context, domain string, id int) error {
 124  	endpoint := c.BaseURL.JoinPath("dns", "delete", domain, strconv.Itoa(id))
 125  
 126  	respBody, err := c.do(ctx, endpoint, nil)
 127  	if err != nil {
 128  		return err
 129  	}
 130  
 131  	statusResp := Status{}
 132  	err = json.Unmarshal(respBody, &statusResp)
 133  	if err != nil {
 134  		return fmt.Errorf("failed to unmarshal response: %w", err)
 135  	}
 136  
 137  	if statusResp.Status != statusSuccess {
 138  		return statusResp
 139  	}
 140  
 141  	return nil
 142  }
 143  
 144  // RetrieveRecords retrieve all editable DNS records associated with a domain.
 145  func (c *Client) RetrieveRecords(ctx context.Context, domain string) ([]Record, error) {
 146  	endpoint := c.BaseURL.JoinPath("dns", "retrieve", domain)
 147  
 148  	respBody, err := c.do(ctx, endpoint, nil)
 149  	if err != nil {
 150  		return nil, err
 151  	}
 152  
 153  	retrieveResp := retrieveResponse{}
 154  	err = json.Unmarshal(respBody, &retrieveResp)
 155  	if err != nil {
 156  		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
 157  	}
 158  
 159  	if retrieveResp.Status.Status != statusSuccess {
 160  		return nil, retrieveResp.Status
 161  	}
 162  
 163  	return retrieveResp.Records, nil
 164  }
 165  
 166  // RetrieveSSLBundle retrieve the SSL certificate bundle for the domain.
 167  func (c *Client) RetrieveSSLBundle(ctx context.Context, domain string) (SSLBundle, error) {
 168  	endpoint := c.BaseURL.JoinPath("ssl", "retrieve", domain)
 169  
 170  	respBody, err := c.do(ctx, endpoint, nil)
 171  	if err != nil {
 172  		return SSLBundle{}, err
 173  	}
 174  
 175  	bundleResp := sslBundleResponse{}
 176  	err = json.Unmarshal(respBody, &bundleResp)
 177  	if err != nil {
 178  		return SSLBundle{}, fmt.Errorf("failed to unmarshal response: %w", err)
 179  	}
 180  
 181  	if bundleResp.Status.Status != statusSuccess {
 182  		return SSLBundle{}, bundleResp.Status
 183  	}
 184  
 185  	return bundleResp.SSLBundle, nil
 186  }
 187  
 188  func (c *Client) do(ctx context.Context, endpoint *url.URL, apiRequest interface{}) ([]byte, error) {
 189  	request := authRequest{
 190  		APIKey:       c.apiKey,
 191  		SecretAPIKey: c.secretAPIKey,
 192  		apiRequest:   apiRequest,
 193  	}
 194  
 195  	reqBody, err := json.Marshal(request)
 196  	if err != nil {
 197  		return nil, fmt.Errorf("failed to marshal request body: %w", err)
 198  	}
 199  
 200  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(reqBody))
 201  	if err != nil {
 202  		return nil, fmt.Errorf("failed to create request: %w", err)
 203  	}
 204  
 205  	resp, err := c.HTTPClient.Do(req)
 206  	if err != nil {
 207  		return nil, fmt.Errorf("failed to call API: %w", err)
 208  	}
 209  
 210  	defer func() { _ = resp.Body.Close() }()
 211  
 212  	respBody, err := io.ReadAll(resp.Body)
 213  	if err != nil {
 214  		return nil, fmt.Errorf("failed to read response body: %w", err)
 215  	}
 216  
 217  	switch resp.StatusCode {
 218  	case http.StatusOK:
 219  		return respBody, nil
 220  
 221  	case http.StatusServiceUnavailable:
 222  		// related to https://github.com/nrdcg/porkbun/issues/5
 223  		return nil, &ServerError{
 224  			StatusCode: resp.StatusCode,
 225  			Message:    http.StatusText(http.StatusServiceUnavailable),
 226  		}
 227  
 228  	default:
 229  		return nil, &ServerError{
 230  			StatusCode: resp.StatusCode,
 231  			Message:    string(respBody),
 232  		}
 233  	}
 234  }
 235