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.
4 package dnspod
5 6 import (
7 "encoding/json"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "net/url"
13 "strings"
14 "time"
15 )
16 17 const (
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.
30 type 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 41 func (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.
64 type Status struct {
65 Code string `json:"code,omitempty"`
66 Message string `json:"message,omitempty"`
67 CreatedAt string `json:"created_at,omitempty"`
68 }
69 70 type service struct {
71 client *Client
72 }
73 74 // Client is the DNSPod client.
75 type 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.
98 func 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.
130 func (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 145 func (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.
154 func (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.
184 type Response struct {
185 *http.Response
186 }
187 188 // An ErrorResponse represents an error caused by an API request.
189 type 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.
195 func (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.
204 func 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.
219 type Date struct {
220 time.Time
221 }
222 223 // UnmarshalJSON handles the deserialization of the custom Date type.
224 func (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