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/challenge/dns01"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const baseURL = "https://api.wedos.com/wapi/json"
  18  
  19  // Client the API client for Webos.
  20  type Client struct {
  21  	username string
  22  	password string
  23  
  24  	baseURL    string
  25  	HTTPClient *http.Client
  26  }
  27  
  28  // NewClient creates a new Client.
  29  func NewClient(username, password string) *Client {
  30  	return &Client{
  31  		username:   username,
  32  		password:   password,
  33  		baseURL:    baseURL,
  34  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  35  	}
  36  }
  37  
  38  // GetRecords lists all the records in the zone.
  39  // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/
  40  func (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) {
  41  	payload := map[string]any{
  42  		"domain": dns01.UnFqdn(zone),
  43  	}
  44  
  45  	req, err := c.newRequest(ctx, commandDNSRowsList, payload)
  46  	if err != nil {
  47  		return nil, err
  48  	}
  49  
  50  	result := APIResponse[Rows]{}
  51  
  52  	err = c.do(req, &result)
  53  	if err != nil {
  54  		return nil, err
  55  	}
  56  
  57  	return result.Response.Data.Rows, err
  58  }
  59  
  60  // AddRecord adds a record in the zone, either by updating existing records or creating new ones.
  61  // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/
  62  // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/
  63  func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error {
  64  	payload := DNSRowRequest{
  65  		Domain: dns01.UnFqdn(zone),
  66  		TTL:    record.TTL,
  67  		Type:   record.Type,
  68  		Data:   record.Data,
  69  	}
  70  
  71  	cmd := commandDNSRowAdd
  72  
  73  	if record.ID == "" {
  74  		payload.Name = record.Name
  75  	} else {
  76  		cmd = commandDNSRowUpdate
  77  		payload.ID = record.ID
  78  	}
  79  
  80  	req, err := c.newRequest(ctx, cmd, payload)
  81  	if err != nil {
  82  		return err
  83  	}
  84  
  85  	return c.do(req, &APIResponse[json.RawMessage]{})
  86  }
  87  
  88  // DeleteRecord deletes a record from the zone.
  89  // If a record does not have an ID, it will be looked up.
  90  // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/
  91  func (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {
  92  	payload := DNSRowRequest{
  93  		Domain: dns01.UnFqdn(zone),
  94  		ID:     recordID,
  95  	}
  96  
  97  	req, err := c.newRequest(ctx, commandDNSRowDelete, payload)
  98  	if err != nil {
  99  		return err
 100  	}
 101  
 102  	return c.do(req, &APIResponse[json.RawMessage]{})
 103  }
 104  
 105  // Commit not really required, all changes will be auto-committed after 5 minutes.
 106  // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/
 107  func (c *Client) Commit(ctx context.Context, zone string) error {
 108  	payload := map[string]any{
 109  		"name": dns01.UnFqdn(zone),
 110  	}
 111  
 112  	req, err := c.newRequest(ctx, commandDNSDomainCommit, payload)
 113  	if err != nil {
 114  		return err
 115  	}
 116  
 117  	return c.do(req, &APIResponse[json.RawMessage]{})
 118  }
 119  
 120  func (c *Client) Ping(ctx context.Context) error {
 121  	req, err := c.newRequest(ctx, commandPing, nil)
 122  	if err != nil {
 123  		return err
 124  	}
 125  
 126  	return c.do(req, &APIResponse[json.RawMessage]{})
 127  }
 128  
 129  func (c *Client) do(req *http.Request, result Response) error {
 130  	resp, err := c.HTTPClient.Do(req)
 131  	if err != nil {
 132  		return errutils.NewHTTPDoError(req, err)
 133  	}
 134  
 135  	raw, err := io.ReadAll(resp.Body)
 136  	if err != nil {
 137  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 138  	}
 139  
 140  	if resp.StatusCode/100 != 2 {
 141  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 142  	}
 143  
 144  	err = json.Unmarshal(raw, result)
 145  	if err != nil {
 146  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 147  	}
 148  
 149  	if result.GetCode() != codeOk {
 150  		return fmt.Errorf("error %d: %s", result.GetCode(), result.GetResult())
 151  	}
 152  
 153  	return err
 154  }
 155  
 156  func (c *Client) newRequest(ctx context.Context, command string, payload any) (*http.Request, error) {
 157  	requestObject := map[string]any{
 158  		"request": APIRequest{
 159  			User:    c.username,
 160  			Auth:    authToken(c.username, c.password),
 161  			Command: command,
 162  			Data:    payload,
 163  		},
 164  	}
 165  
 166  	object, err := json.Marshal(requestObject)
 167  	if err != nil {
 168  		return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 169  	}
 170  
 171  	form := url.Values{}
 172  	form.Add("request", string(object))
 173  
 174  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode()))
 175  	if err != nil {
 176  		return nil, fmt.Errorf("unable to create request: %w", err)
 177  	}
 178  
 179  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 180  
 181  	return req, nil
 182  }
 183