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