client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"fmt"
   7  	"io"
   8  	"net/http"
   9  	"net/url"
  10  	"strings"
  11  	"time"
  12  
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  14  )
  15  
  16  const defaultBaseURL = "https://api.reg.ru/api/regru2/"
  17  
  18  // Client the reg.ru client.
  19  type Client struct {
  20  	username string
  21  	password string
  22  
  23  	baseURL    *url.URL
  24  	HTTPClient *http.Client
  25  }
  26  
  27  // NewClient Creates a reg.ru client.
  28  func NewClient(username, password string) *Client {
  29  	baseURL, _ := url.Parse(defaultBaseURL)
  30  
  31  	return &Client{
  32  		username:   username,
  33  		password:   password,
  34  		baseURL:    baseURL,
  35  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  36  	}
  37  }
  38  
  39  // RemoveTxtRecord removes a TXT record.
  40  // https://www.reg.ru/support/help/api2#zone_remove_record
  41  func (c *Client) RemoveTxtRecord(ctx context.Context, domain, subDomain, content string) error {
  42  	request := RemoveRecordRequest{
  43  		Domains:           []Domain{{DName: domain}},
  44  		SubDomain:         subDomain,
  45  		Content:           content,
  46  		RecordType:        "TXT",
  47  		OutputContentType: "plain",
  48  	}
  49  
  50  	resp, err := c.doRequest(ctx, request, "zone", "remove_record")
  51  	if err != nil {
  52  		return err
  53  	}
  54  
  55  	return resp.HasError()
  56  }
  57  
  58  // AddTXTRecord adds a TXT record.
  59  // https://www.reg.ru/support/help/api2#zone_add_txt
  60  func (c *Client) AddTXTRecord(ctx context.Context, domain, subDomain, content string) error {
  61  	request := AddTxtRequest{
  62  		Domains:           []Domain{{DName: domain}},
  63  		SubDomain:         subDomain,
  64  		Text:              content,
  65  		OutputContentType: "plain",
  66  	}
  67  
  68  	resp, err := c.doRequest(ctx, request, "zone", "add_txt")
  69  	if err != nil {
  70  		return err
  71  	}
  72  
  73  	return resp.HasError()
  74  }
  75  
  76  func (c *Client) doRequest(ctx context.Context, request any, fragments ...string) (*APIResponse, error) {
  77  	endpoint := c.baseURL.JoinPath(fragments...)
  78  
  79  	inputData, err := json.Marshal(request)
  80  	if err != nil {
  81  		return nil, fmt.Errorf("failed to create input data: %w", err)
  82  	}
  83  
  84  	data := url.Values{}
  85  	data.Set("username", c.username)
  86  	data.Set("password", c.password)
  87  	data.Set("input_data", string(inputData))
  88  	data.Set("input_format", "json")
  89  
  90  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))
  91  	if err != nil {
  92  		return nil, fmt.Errorf("unable to create request: %w", err)
  93  	}
  94  
  95  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  96  
  97  	resp, err := c.HTTPClient.Do(req)
  98  	if err != nil {
  99  		return nil, errutils.NewHTTPDoError(req, err)
 100  	}
 101  
 102  	defer func() { _ = resp.Body.Close() }()
 103  
 104  	if resp.StatusCode/100 != 2 {
 105  		return nil, parseError(req, resp)
 106  	}
 107  
 108  	raw, err := io.ReadAll(resp.Body)
 109  	if err != nil {
 110  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
 111  	}
 112  
 113  	var apiResp APIResponse
 114  
 115  	err = json.Unmarshal(raw, &apiResp)
 116  	if err != nil {
 117  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 118  	}
 119  
 120  	return &apiResp, nil
 121  }
 122  
 123  func parseError(req *http.Request, resp *http.Response) error {
 124  	raw, _ := io.ReadAll(resp.Body)
 125  
 126  	var errAPI APIResponse
 127  
 128  	err := json.Unmarshal(raw, &errAPI)
 129  	if err != nil {
 130  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 131  	}
 132  
 133  	return fmt.Errorf("status code: %d, %w", resp.StatusCode, errAPI)
 134  }
 135