client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/xml"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  // Client the Plesk API client.
  18  type Client struct {
  19  	login    string
  20  	password string
  21  
  22  	baseURL    *url.URL
  23  	HTTPClient *http.Client
  24  }
  25  
  26  // NewClient created a new Client.
  27  func NewClient(baseURL *url.URL, login, password string) *Client {
  28  	return &Client{
  29  		login:      login,
  30  		password:   password,
  31  		baseURL:    baseURL,
  32  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  33  	}
  34  }
  35  
  36  // GetSite gets a site.
  37  // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-sites-domains/getting-information-about-sites.66583/
  38  func (c *Client) GetSite(ctx context.Context, domain string) (int, error) {
  39  	payload := RequestPacketType{Site: &SiteTypeRequest{Get: SiteGetRequest{Filter: &SiteFilterType{
  40  		Name: domain,
  41  	}}}}
  42  
  43  	response, err := c.doRequest(ctx, payload)
  44  	if err != nil {
  45  		return 0, err
  46  	}
  47  
  48  	if response.System != nil {
  49  		return 0, response.System
  50  	}
  51  
  52  	if response == nil || response.Site.Get.Result == nil {
  53  		return 0, errors.New("unexpected empty result")
  54  	}
  55  
  56  	if response.Site.Get.Result.Status != StatusOK {
  57  		return 0, response.Site.Get.Result
  58  	}
  59  
  60  	return response.Site.Get.Result.ID, nil
  61  }
  62  
  63  // AddRecord adds a TXT record.
  64  // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798/
  65  func (c *Client) AddRecord(ctx context.Context, siteID int, host, value string) (int, error) {
  66  	payload := RequestPacketType{DNS: &DNSInputType{AddRec: []AddRecRequest{{
  67  		SiteID: siteID,
  68  		Type:   "TXT",
  69  		Host:   host,
  70  		Value:  value,
  71  	}}}}
  72  
  73  	response, err := c.doRequest(ctx, payload)
  74  	if err != nil {
  75  		return 0, err
  76  	}
  77  
  78  	if response.System != nil {
  79  		return 0, response.System
  80  	}
  81  
  82  	if len(response.DNS.AddRec) < 1 {
  83  		return 0, errors.New("unexpected empty result")
  84  	}
  85  
  86  	if response.DNS.AddRec[0].Result.Status != StatusOK {
  87  		return 0, response.DNS.AddRec[0].Result
  88  	}
  89  
  90  	return response.DNS.AddRec[0].Result.ID, nil
  91  }
  92  
  93  // DeleteRecord Deletes a TXT record.
  94  // https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/deleting-dns-records.34864/
  95  func (c *Client) DeleteRecord(ctx context.Context, recordID int) (int, error) {
  96  	payload := RequestPacketType{DNS: &DNSInputType{DelRec: []DelRecRequest{{Filter: DNSSelectionFilterType{
  97  		ID: recordID,
  98  	}}}}}
  99  
 100  	response, err := c.doRequest(ctx, payload)
 101  	if err != nil {
 102  		return 0, err
 103  	}
 104  
 105  	if response.System != nil {
 106  		return 0, response.System
 107  	}
 108  
 109  	if len(response.DNS.DelRec) < 1 {
 110  		return 0, errors.New("unexpected empty result")
 111  	}
 112  
 113  	if response.DNS.DelRec[0].Result.Status != StatusOK {
 114  		return 0, response.DNS.DelRec[0].Result
 115  	}
 116  
 117  	return response.DNS.DelRec[0].Result.ID, nil
 118  }
 119  
 120  func (c *Client) doRequest(ctx context.Context, payload RequestPacketType) (*ResponsePacketType, error) {
 121  	endpoint := c.baseURL.JoinPath("/enterprise/control/agent.php")
 122  
 123  	body := new(bytes.Buffer)
 124  
 125  	err := xml.NewEncoder(body).Encode(payload)
 126  	if err != nil {
 127  		return nil, err
 128  	}
 129  
 130  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), body)
 131  	if err != nil {
 132  		return nil, fmt.Errorf("unable to create request: %w", err)
 133  	}
 134  
 135  	req.Header.Set("Content-Type", "text/xml")
 136  
 137  	req.Header.Set("Http_auth_login", c.login)
 138  	req.Header.Set("Http_auth_passwd", c.password)
 139  
 140  	resp, err := c.HTTPClient.Do(req)
 141  	if err != nil {
 142  		return nil, errutils.NewHTTPDoError(req, err)
 143  	}
 144  
 145  	defer func() { _ = resp.Body.Close() }()
 146  
 147  	if resp.StatusCode/100 != 2 {
 148  		return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 149  	}
 150  
 151  	raw, err := io.ReadAll(resp.Body)
 152  	if err != nil {
 153  		return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
 154  	}
 155  
 156  	var response ResponsePacketType
 157  
 158  	err = xml.Unmarshal(raw, &response)
 159  	if err != nil {
 160  		return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 161  	}
 162  
 163  	return &response, nil
 164  }
 165