client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"sync"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  )
  17  
  18  // Default API endpoints.
  19  const (
  20  	APIBaseURL  = "https://console.cloud.ru/api/clouddns/v1"
  21  	AuthBaseURL = "https://auth.iam.cloud.ru/auth/system/openid/token"
  22  )
  23  
  24  // Client the Cloud.ru API client.
  25  type Client struct {
  26  	keyID  string
  27  	secret string
  28  
  29  	APIEndpoint  *url.URL
  30  	AuthEndpoint *url.URL
  31  	HTTPClient   *http.Client
  32  
  33  	token   *Token
  34  	muToken sync.Mutex
  35  }
  36  
  37  // NewClient Creates a new Client.
  38  func NewClient(login, secret string) *Client {
  39  	apiEndpoint, _ := url.Parse(APIBaseURL)
  40  	authEndpoint, _ := url.Parse(AuthBaseURL)
  41  
  42  	return &Client{
  43  		keyID:        login,
  44  		secret:       secret,
  45  		APIEndpoint:  apiEndpoint,
  46  		AuthEndpoint: authEndpoint,
  47  		HTTPClient:   &http.Client{Timeout: 5 * time.Second},
  48  	}
  49  }
  50  
  51  func (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) {
  52  	endpoint := c.APIEndpoint.JoinPath("zones")
  53  
  54  	query := endpoint.Query()
  55  	query.Set("parentId", parentID)
  56  	endpoint.RawQuery = query.Encode()
  57  
  58  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  59  	if err != nil {
  60  		return nil, err
  61  	}
  62  
  63  	var zones APIResponse[Zone]
  64  
  65  	err = c.do(req, &zones)
  66  	if err != nil {
  67  		return nil, err
  68  	}
  69  
  70  	return zones.Items, nil
  71  }
  72  
  73  func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error) {
  74  	endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
  75  
  76  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  77  	if err != nil {
  78  		return nil, err
  79  	}
  80  
  81  	var records APIResponse[Record]
  82  
  83  	err = c.do(req, &records)
  84  	if err != nil {
  85  		return nil, err
  86  	}
  87  
  88  	return records.Items, nil
  89  }
  90  
  91  func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {
  92  	endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records")
  93  
  94  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
  95  	if err != nil {
  96  		return nil, err
  97  	}
  98  
  99  	var result Record
 100  
 101  	err = c.do(req, &result)
 102  	if err != nil {
 103  		return nil, err
 104  	}
 105  
 106  	return &result, nil
 107  }
 108  
 109  func (c *Client) DeleteRecord(ctx context.Context, zoneID, name, recordType string) error {
 110  	endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records", name, recordType)
 111  
 112  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 113  	if err != nil {
 114  		return err
 115  	}
 116  
 117  	return c.do(req, nil)
 118  }
 119  
 120  func (c *Client) do(req *http.Request, result any) error {
 121  	tok := getToken(req.Context())
 122  	if tok != nil {
 123  		req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
 124  	} else {
 125  		return errors.New("not logged in")
 126  	}
 127  
 128  	resp, err := c.HTTPClient.Do(req)
 129  	if err != nil {
 130  		return errutils.NewHTTPDoError(req, err)
 131  	}
 132  
 133  	defer func() { _ = resp.Body.Close() }()
 134  
 135  	if resp.StatusCode != http.StatusOK {
 136  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 137  	}
 138  
 139  	raw, err := io.ReadAll(resp.Body)
 140  	if err != nil {
 141  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 142  	}
 143  
 144  	if result == nil {
 145  		return nil
 146  	}
 147  
 148  	err = json.Unmarshal(raw, result)
 149  	if err != nil {
 150  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 151  	}
 152  
 153  	return nil
 154  }
 155  
 156  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 157  	buf := new(bytes.Buffer)
 158  
 159  	if payload != nil {
 160  		err := json.NewEncoder(buf).Encode(payload)
 161  		if err != nil {
 162  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 163  		}
 164  	}
 165  
 166  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 167  	if err != nil {
 168  		return nil, fmt.Errorf("unable to create request: %w", err)
 169  	}
 170  
 171  	req.Header.Set("Accept", "application/json")
 172  
 173  	if payload != nil {
 174  		req.Header.Set("Content-Type", "application/json")
 175  	}
 176  
 177  	return req, nil
 178  }
 179