client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"fmt"
   8  	"net/http"
   9  	"strconv"
  10  	"strings"
  11  	"sync"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  	"github.com/go-viper/mapstructure/v2"
  16  )
  17  
  18  const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
  19  
  20  type Authentication interface {
  21  	Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error)
  22  }
  23  
  24  // Client a KAS server client.
  25  type Client struct {
  26  	login string
  27  
  28  	floodTime   time.Time
  29  	muFloodTime sync.Mutex
  30  
  31  	baseURL    string
  32  	HTTPClient *http.Client
  33  }
  34  
  35  // NewClient creates a new Client.
  36  func NewClient(login string) *Client {
  37  	return &Client{
  38  		login:      login,
  39  		baseURL:    apiEndpoint,
  40  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  41  	}
  42  }
  43  
  44  // GetDNSSettings Reading out the DNS settings of a zone.
  45  // - zone: host zone.
  46  // - recordID: the ID of the resource record (optional).
  47  func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]ReturnInfo, error) {
  48  	requestParams := map[string]string{"zone_host": zone}
  49  
  50  	if recordID != "" {
  51  		requestParams["record_id"] = recordID
  52  	}
  53  
  54  	req, err := c.newRequest(ctx, "get_dns_settings", requestParams)
  55  	if err != nil {
  56  		return nil, err
  57  	}
  58  
  59  	var g GetDNSSettingsAPIResponse
  60  
  61  	err = c.do(req, &g)
  62  	if err != nil {
  63  		return nil, err
  64  	}
  65  
  66  	c.updateFloodTime(g.Response.KasFloodDelay)
  67  
  68  	return g.Response.ReturnInfo, nil
  69  }
  70  
  71  // AddDNSSettings Creation of a DNS resource record.
  72  func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) {
  73  	req, err := c.newRequest(ctx, "add_dns_settings", record)
  74  	if err != nil {
  75  		return "", err
  76  	}
  77  
  78  	var g AddDNSSettingsAPIResponse
  79  
  80  	err = c.do(req, &g)
  81  	if err != nil {
  82  		return "", err
  83  	}
  84  
  85  	c.updateFloodTime(g.Response.KasFloodDelay)
  86  
  87  	return g.Response.ReturnInfo, nil
  88  }
  89  
  90  // DeleteDNSSettings Deleting a DNS Resource Record.
  91  func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) {
  92  	requestParams := map[string]string{"record_id": recordID}
  93  
  94  	req, err := c.newRequest(ctx, "delete_dns_settings", requestParams)
  95  	if err != nil {
  96  		return "", err
  97  	}
  98  
  99  	var g DeleteDNSSettingsAPIResponse
 100  
 101  	err = c.do(req, &g)
 102  	if err != nil {
 103  		return "", err
 104  	}
 105  
 106  	c.updateFloodTime(g.Response.KasFloodDelay)
 107  
 108  	return g.Response.ReturnString, nil
 109  }
 110  
 111  func (c *Client) newRequest(ctx context.Context, action string, requestParams any) (*http.Request, error) {
 112  	ar := KasRequest{
 113  		Login:         c.login,
 114  		AuthType:      "session",
 115  		AuthData:      getToken(ctx),
 116  		Action:        action,
 117  		RequestParams: requestParams,
 118  	}
 119  
 120  	body, err := json.Marshal(ar)
 121  	if err != nil {
 122  		return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 123  	}
 124  
 125  	payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
 126  
 127  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload))
 128  	if err != nil {
 129  		return nil, fmt.Errorf("unable to create request: %w", err)
 130  	}
 131  
 132  	return req, nil
 133  }
 134  
 135  func (c *Client) do(req *http.Request, result any) error {
 136  	c.muFloodTime.Lock()
 137  	time.Sleep(time.Until(c.floodTime))
 138  	c.muFloodTime.Unlock()
 139  
 140  	resp, err := c.HTTPClient.Do(req)
 141  	if err != nil {
 142  		return errutils.NewHTTPDoError(req, err)
 143  	}
 144  
 145  	defer func() { _ = resp.Body.Close() }()
 146  
 147  	if resp.StatusCode != http.StatusOK {
 148  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 149  	}
 150  
 151  	envlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body)
 152  	if err != nil {
 153  		return err
 154  	}
 155  
 156  	if envlp.Body.Fault != nil {
 157  		return envlp.Body.Fault
 158  	}
 159  
 160  	raw := getValue(envlp.Body.KasAPIResponse.Return)
 161  
 162  	err = mapstructure.Decode(raw, result)
 163  	if err != nil {
 164  		return fmt.Errorf("response struct decode: %w", err)
 165  	}
 166  
 167  	return nil
 168  }
 169  
 170  func (c *Client) updateFloodTime(delay float64) {
 171  	c.muFloodTime.Lock()
 172  	c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))
 173  	c.muFloodTime.Unlock()
 174  }
 175  
 176  func getValue(item *Item) any {
 177  	switch {
 178  	case item.Raw != "":
 179  		v, _ := strconv.ParseBool(item.Raw)
 180  		return v
 181  
 182  	case item.Text != "":
 183  		switch item.Type {
 184  		case "xsd:string":
 185  			return item.Text
 186  		case "xsd:float":
 187  			v, _ := strconv.ParseFloat(item.Text, 64)
 188  			return v
 189  		case "xsd:int":
 190  			v, _ := strconv.ParseInt(item.Text, 10, 64)
 191  			return v
 192  		default:
 193  			return item.Text
 194  		}
 195  
 196  	case item.Value != nil:
 197  		return getValue(item.Value)
 198  
 199  	case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array":
 200  		var v []any
 201  		for _, i := range item.Items {
 202  			v = append(v, getValue(i))
 203  		}
 204  
 205  		return v
 206  
 207  	case len(item.Items) > 0:
 208  		v := map[string]any{}
 209  		for _, i := range item.Items {
 210  			v[getKey(i)] = getValue(i)
 211  		}
 212  
 213  		return v
 214  
 215  	default:
 216  		return ""
 217  	}
 218  }
 219  
 220  func getKey(item *Item) string {
 221  	if item.Key == nil {
 222  		return ""
 223  	}
 224  
 225  	return item.Key.Text
 226  }
 227