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  	"strconv"
  13  	"strings"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  )
  17  
  18  const defaultBaseURL = "https://clouddns.manageengine.com/v1"
  19  
  20  // Client the ManageEngine CloudDNS API client.
  21  type Client struct {
  22  	baseURL    *url.URL
  23  	httpClient *http.Client
  24  }
  25  
  26  // NewClient creates a new Client.
  27  func NewClient(hc *http.Client) *Client {
  28  	baseURL, _ := url.Parse(defaultBaseURL)
  29  
  30  	return &Client{
  31  		baseURL:    baseURL,
  32  		httpClient: hc,
  33  	}
  34  }
  35  
  36  // GetAllZones gets all zones.
  37  // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All
  38  func (c *Client) GetAllZones(ctx context.Context) ([]Zone, error) {
  39  	endpoint := c.baseURL.JoinPath("dns", "domain")
  40  
  41  	req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
  42  	if err != nil {
  43  		return nil, err
  44  	}
  45  
  46  	var results []Zone
  47  
  48  	err = c.do(req, &results)
  49  	if err != nil {
  50  		return nil, err
  51  	}
  52  
  53  	return results, nil
  54  }
  55  
  56  // GetAllZoneRecords gets all "zone records" for a zone.
  57  // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All_9
  58  func (c *Client) GetAllZoneRecords(ctx context.Context, zoneID int) ([]ZoneRecord, error) {
  59  	endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT")
  60  
  61  	req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
  62  	if err != nil {
  63  		return nil, err
  64  	}
  65  
  66  	var results []ZoneRecord
  67  
  68  	err = c.do(req, &results)
  69  	if err != nil {
  70  		return nil, err
  71  	}
  72  
  73  	return results, nil
  74  }
  75  
  76  // DeleteZoneRecord deletes a "zone record".
  77  // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#DEL_Delete_10
  78  func (c *Client) DeleteZoneRecord(ctx context.Context, zoneID, domainID int) error {
  79  	endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", strconv.Itoa(domainID))
  80  
  81  	req, err := newRequest(ctx, http.MethodDelete, endpoint, nil)
  82  	if err != nil {
  83  		return err
  84  	}
  85  
  86  	var results APIResponse
  87  
  88  	return c.do(req, &results)
  89  }
  90  
  91  // CreateZoneRecord creates a "zone record".
  92  // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#POST_Create_10
  93  func (c *Client) CreateZoneRecord(ctx context.Context, zoneID int, record ZoneRecord) error {
  94  	endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", "/")
  95  
  96  	req, err := newRequest(ctx, http.MethodPost, endpoint, []ZoneRecord{record})
  97  	if err != nil {
  98  		return err
  99  	}
 100  
 101  	var results APIResponse
 102  
 103  	return c.do(req, &results)
 104  }
 105  
 106  // UpdateZoneRecord update an existing "zone record".
 107  // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#PUT_Update_10
 108  func (c *Client) UpdateZoneRecord(ctx context.Context, record ZoneRecord) error {
 109  	if record.SpfTxtDomainID == 0 {
 110  		return errors.New("SpfTxtDomainID is empty")
 111  	}
 112  
 113  	if record.ZoneID == 0 {
 114  		return errors.New("ZoneID is empty")
 115  	}
 116  
 117  	endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(record.ZoneID), "records", "SPF_TXT", strconv.Itoa(record.SpfTxtDomainID), "/")
 118  
 119  	req, err := newRequest(ctx, http.MethodPut, endpoint, []ZoneRecord{record})
 120  	if err != nil {
 121  		return err
 122  	}
 123  
 124  	var results APIResponse
 125  
 126  	return c.do(req, &results)
 127  }
 128  
 129  func (c *Client) do(req *http.Request, result any) error {
 130  	resp, err := c.httpClient.Do(req)
 131  	if err != nil {
 132  		return errutils.NewHTTPDoError(req, err)
 133  	}
 134  
 135  	defer func() { _ = resp.Body.Close() }()
 136  
 137  	if resp.StatusCode/100 != 2 {
 138  		return parseError(req, resp)
 139  	}
 140  
 141  	if result == nil {
 142  		return nil
 143  	}
 144  
 145  	raw, err := io.ReadAll(resp.Body)
 146  	if err != nil {
 147  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 148  	}
 149  
 150  	err = json.Unmarshal(raw, result)
 151  	if err != nil {
 152  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 153  	}
 154  
 155  	return nil
 156  }
 157  
 158  func newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 159  	var body io.Reader = http.NoBody
 160  
 161  	if payload != nil {
 162  		buf := new(bytes.Buffer)
 163  
 164  		err := json.NewEncoder(buf).Encode(payload)
 165  		if err != nil {
 166  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 167  		}
 168  
 169  		values := url.Values{}
 170  		values.Set("config", buf.String())
 171  		body = strings.NewReader(values.Encode())
 172  	}
 173  
 174  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
 175  	if err != nil {
 176  		return nil, fmt.Errorf("unable to create request: %w", err)
 177  	}
 178  
 179  	req.Header.Set("Accept", "application/json")
 180  
 181  	if payload != nil {
 182  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 183  	}
 184  
 185  	return req, nil
 186  }
 187  
 188  func parseError(req *http.Request, resp *http.Response) error {
 189  	raw, _ := io.ReadAll(resp.Body)
 190  
 191  	var errAPI APIError
 192  
 193  	err := json.Unmarshal(raw, &errAPI)
 194  	if err != nil {
 195  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 196  	}
 197  
 198  	return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI)
 199  }
 200