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