dnspod.go raw

   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