client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"time"
  12  
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  14  	"golang.org/x/oauth2"
  15  )
  16  
  17  const defaultBaseURL = "https://dns-service.iran.liara.ir"
  18  
  19  // Client a Liara DNS API client.
  20  type Client struct {
  21  	baseURL    *url.URL
  22  	httpClient *http.Client
  23  }
  24  
  25  // NewClient creates a new Client.
  26  func NewClient(hc *http.Client) *Client {
  27  	baseURL, _ := url.Parse(defaultBaseURL)
  28  
  29  	if hc == nil {
  30  		hc = &http.Client{Timeout: 10 * time.Second}
  31  	}
  32  
  33  	return &Client{httpClient: hc, baseURL: baseURL}
  34  }
  35  
  36  // GetRecords gets the records of a domain.
  37  // https://openapi.liara.ir/?urls.primaryName=DNS
  38  func (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) {
  39  	endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
  40  
  41  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  42  	if err != nil {
  43  		return nil, fmt.Errorf("create request: %w", err)
  44  	}
  45  
  46  	resp, err := c.httpClient.Do(req)
  47  	if err != nil {
  48  		return nil, errutils.NewHTTPDoError(req, err)
  49  	}
  50  
  51  	defer func() { _ = resp.Body.Close() }()
  52  
  53  	if resp.StatusCode != http.StatusOK {
  54  		return nil, parseError(req, resp)
  55  	}
  56  
  57  	raw, err := io.ReadAll(resp.Body)
  58  	if err != nil {
  59  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
  60  	}
  61  
  62  	var response Response[[]Record]
  63  
  64  	err = json.Unmarshal(raw, &response)
  65  	if err != nil {
  66  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
  67  	}
  68  
  69  	return response.Data, nil
  70  }
  71  
  72  // CreateRecord creates a record.
  73  func (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) {
  74  	endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records")
  75  
  76  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
  77  	if err != nil {
  78  		return nil, fmt.Errorf("create request: %w", err)
  79  	}
  80  
  81  	resp, err := c.httpClient.Do(req)
  82  	if err != nil {
  83  		return nil, errutils.NewHTTPDoError(req, err)
  84  	}
  85  
  86  	defer func() { _ = resp.Body.Close() }()
  87  
  88  	if resp.StatusCode != http.StatusCreated {
  89  		return nil, parseError(req, resp)
  90  	}
  91  
  92  	raw, err := io.ReadAll(resp.Body)
  93  	if err != nil {
  94  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
  95  	}
  96  
  97  	var response Response[*Record]
  98  
  99  	err = json.Unmarshal(raw, &response)
 100  	if err != nil {
 101  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 102  	}
 103  
 104  	return response.Data, nil
 105  }
 106  
 107  // GetRecord gets a specific record.
 108  func (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) {
 109  	endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
 110  
 111  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
 112  	if err != nil {
 113  		return nil, fmt.Errorf("create request: %w", err)
 114  	}
 115  
 116  	resp, err := c.httpClient.Do(req)
 117  	if err != nil {
 118  		return nil, errutils.NewHTTPDoError(req, err)
 119  	}
 120  
 121  	defer func() { _ = resp.Body.Close() }()
 122  
 123  	if resp.StatusCode != http.StatusOK {
 124  		return nil, parseError(req, resp)
 125  	}
 126  
 127  	raw, err := io.ReadAll(resp.Body)
 128  	if err != nil {
 129  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
 130  	}
 131  
 132  	var response Response[*Record]
 133  
 134  	err = json.Unmarshal(raw, &response)
 135  	if err != nil {
 136  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 137  	}
 138  
 139  	return response.Data, nil
 140  }
 141  
 142  // DeleteRecord deletes a record.
 143  func (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error {
 144  	endpoint := c.baseURL.JoinPath("api", "v1", "zones", domainName, "dns-records", recordID)
 145  
 146  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 147  	if err != nil {
 148  		return fmt.Errorf("create request: %w", err)
 149  	}
 150  
 151  	resp, err := c.httpClient.Do(req)
 152  	if err != nil {
 153  		return errutils.NewHTTPDoError(req, err)
 154  	}
 155  
 156  	defer func() { _ = resp.Body.Close() }()
 157  
 158  	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
 159  		return parseError(req, resp)
 160  	}
 161  
 162  	return nil
 163  }
 164  
 165  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 166  	buf := new(bytes.Buffer)
 167  
 168  	if payload != nil {
 169  		err := json.NewEncoder(buf).Encode(payload)
 170  		if err != nil {
 171  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 172  		}
 173  	}
 174  
 175  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 176  	if err != nil {
 177  		return nil, fmt.Errorf("unable to create request: %w", err)
 178  	}
 179  
 180  	req.Header.Set("Accept", "application/json")
 181  
 182  	if payload != nil {
 183  		req.Header.Set("Content-Type", "application/json")
 184  	}
 185  
 186  	return req, nil
 187  }
 188  
 189  func parseError(req *http.Request, resp *http.Response) error {
 190  	raw, _ := io.ReadAll(resp.Body)
 191  
 192  	var errAPI APIError
 193  
 194  	err := json.Unmarshal(raw, &errAPI)
 195  	if err != nil {
 196  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 197  	}
 198  
 199  	return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI)
 200  }
 201  
 202  func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
 203  	if client == nil {
 204  		client = &http.Client{Timeout: 5 * time.Second}
 205  	}
 206  
 207  	client.Transport = &oauth2.Transport{
 208  		Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
 209  		Base:   client.Transport,
 210  	}
 211  
 212  	return client
 213  }
 214