1// Package dnspod implements a client for the dnspod API.
2//
3// In order to use this package you will need a dnspod account and your API Token.
4package dnspod
5
6import (
7	"encoding/json"
8	"fmt"
9	"io"
10	"net"
11	"net/http"
12	"net/url"
13	"strings"
14	"time"
15)
16
17const (
18	libraryVersion   = "0.4"
19	defaultBaseURL   = "https://dnsapi.cn/"
20	defaultUserAgent = "dnspod-go/" + libraryVersion
21
22	// apiVersion       = "v1"
23	defaultTimeout   = 5
24	defaultKeepAlive = 30
25)
26
27// dnspod API docs: https://www.dnspod.cn/docs/info.html
28
29// CommonParams is the commons parameters.
30type CommonParams struct {
31	LoginToken   string
32	Format       string
33	Lang         string
34	ErrorOnEmpty string
35	UserID       string
36
37	Timeout   int
38	KeepAlive int
39}
40
41func (c CommonParams) toPayLoad() url.Values {
42	p := url.Values{}
43
44	if c.LoginToken != "" {
45		p.Set("login_token", c.LoginToken)
46	}
47	if c.Format != "" {
48		p.Set("format", c.Format)
49	}
50	if c.Lang != "" {
51		p.Set("lang", c.Lang)
52	}
53	if c.ErrorOnEmpty != "" {
54		p.Set("error_on_empty", c.ErrorOnEmpty)
55	}
56	if c.UserID != "" {
57		p.Set("user_id", c.UserID)
58	}
59
60	return p
61}
62
63// Status is the status representation.
64type Status struct {
65	Code      string `json:"code,omitempty"`
66	Message   string `json:"message,omitempty"`
67	CreatedAt string `json:"created_at,omitempty"`
68}
69
70type service struct {
71	client *Client
72}
73
74// Client is the DNSPod client.
75type Client struct {
76	// HTTP client used to communicate with the API.
77	HTTPClient *http.Client
78
79	// CommonParams used communicating with the dnspod API.
80	CommonParams CommonParams
81
82	// Base URL for API requests.
83	// Defaults to the public dnspod API, but can be set to a different endpoint (e.g. the sandbox).
84	// BaseURL should always be specified with a trailing slash.
85	BaseURL string
86
87	// User agent used when communicating with the dnspod API.
88	UserAgent string
89
90	common service // Reuse a single struct instead of allocating one for each service on the heap.
91
92	// Services used for talking to different parts of the dnspod API.
93	Domains *DomainsService
94	Records *RecordsService
95}
96
97// NewClient returns a new dnspod API client.
98func NewClient(params CommonParams) *Client {
99	timeout := defaultTimeout
100	if params.Timeout != 0 {
101		timeout = params.Timeout
102	}
103
104	keepalive := defaultKeepAlive
105	if params.KeepAlive != 0 {
106		keepalive = params.KeepAlive
107	}
108
109	httpClient := http.Client{
110		Transport: &http.Transport{
111			DialContext: (&net.Dialer{
112				Timeout:   time.Duration(timeout) * time.Second,
113				KeepAlive: time.Duration(keepalive) * time.Second,
114			}).DialContext,
115		},
116	}
117
118	client := &Client{HTTPClient: &httpClient, CommonParams: params, BaseURL: defaultBaseURL, UserAgent: defaultUserAgent}
119
120	client.common.client = client
121	client.Domains = (*DomainsService)(&client.common)
122	client.Records = (*RecordsService)(&client.common)
123
124	return client
125}
126
127// NewRequest creates an API request.
128// The path is expected to be a relative path and will be resolved
129// according to the BaseURL of the Client. Paths should always be specified without a preceding slash.
130func (c *Client) NewRequest(method, path string, payload url.Values) (*http.Request, error) {
131	uri := c.BaseURL + path
132
133	req, err := http.NewRequest(method, uri, strings.NewReader(payload.Encode()))
134	if err != nil {
135		return nil, err
136	}
137
138	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
139	req.Header.Add("Accept", "application/json")
140	req.Header.Add("User-Agent", c.UserAgent)
141
142	return req, nil
143}
144
145func (c *Client) post(path string, payload url.Values, v interface{}) (*Response, error) {
146	return c.Do(http.MethodPost, path, payload, v)
147}
148
149// Do sends an API request and returns the API response.
150// The API response is JSON decoded and stored in the value pointed by v,
151// or returned as an error if an API error has occurred.
152// If v implements the io.Writer interface, the raw response body will be written to v,
153// without attempting to decode it.
154func (c *Client) Do(method, path string, payload url.Values, v interface{}) (*Response, error) {
155	req, err := c.NewRequest(method, path, payload)
156	if err != nil {
157		return nil, err
158	}
159
160	res, err := c.HTTPClient.Do(req)
161	if err != nil {
162		return nil, err
163	}
164	defer func() { _ = res.Body.Close() }()
165
166	response := &Response{Response: res}
167	err = CheckResponse(res)
168	if err != nil {
169		return response, err
170	}
171
172	if v != nil {
173		if w, ok := v.(io.Writer); ok {
174			_, err = io.Copy(w, res.Body)
175		} else {
176			err = json.NewDecoder(res.Body).Decode(v)
177		}
178	}
179
180	return response, err
181}
182
183// A Response represents an API response.
184type Response struct {
185	*http.Response
186}
187
188// An ErrorResponse represents an error caused by an API request.
189type ErrorResponse struct {
190	Response *http.Response // HTTP response that caused this error
191	Message  string         `json:"message"` // human-readable message
192}
193
194// Error implements the error interface.
195func (r *ErrorResponse) Error() string {
196	return fmt.Sprintf("%v %v: %d %v",
197		r.Response.Request.Method, r.Response.Request.URL,
198		r.Response.StatusCode, r.Message)
199}
200
201// CheckResponse checks the API response for errors, and returns them if present.
202// A response is considered an error if the status code is different than 2xx. Specific requests
203// may have additional requirements, but this is sufficient in most of the cases.
204func CheckResponse(r *http.Response) error {
205	if code := r.StatusCode; 200 <= code && code <= 299 {
206		return nil
207	}
208
209	errorResponse := &ErrorResponse{Response: r}
210	err := json.NewDecoder(r.Body).Decode(errorResponse)
211	if err != nil {
212		return err
213	}
214
215	return errorResponse
216}
217
218// Date custom type.
219type Date struct {
220	time.Time
221}
222
223// UnmarshalJSON handles the deserialization of the custom Date type.
224func (d *Date) UnmarshalJSON(data []byte) error {
225	var s string
226	if err := json.Unmarshal(data, &s); err != nil {
227		return fmt.Errorf("date should be a string, got %s: %w", data, err)
228	}
229
230	t, err := time.Parse("2006-01-02", s)
231	if err != nil {
232		return fmt.Errorf("invalid date: %w", err)
233	}
234
235	d.Time = t
236
237	return nil
238}
239