client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"time"
  12  
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  14  )
  15  
  16  type Client struct {
  17  	serverURL  string
  18  	HTTPClient *http.Client
  19  }
  20  
  21  func NewClient(serverURL string) (*Client, error) {
  22  	_, err := url.Parse(serverURL)
  23  	if err != nil {
  24  		return nil, fmt.Errorf("server URL: %w", err)
  25  	}
  26  
  27  	return &Client{
  28  		serverURL:  serverURL,
  29  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  30  	}, nil
  31  }
  32  
  33  func (c *Client) Login(ctx context.Context, username, password string) (string, error) {
  34  	payload := LoginRequest{
  35  		Username:    username,
  36  		Password:    password,
  37  		ClientLogin: false,
  38  	}
  39  
  40  	endpoint, err := url.Parse(c.serverURL)
  41  	if err != nil {
  42  		return "", err
  43  	}
  44  
  45  	endpoint.RawQuery = "login"
  46  
  47  	req, err := newJSONRequest(ctx, endpoint, payload)
  48  	if err != nil {
  49  		return "", err
  50  	}
  51  
  52  	var response APIResponse
  53  
  54  	err = c.do(req, &response)
  55  	if err != nil {
  56  		return "", err
  57  	}
  58  
  59  	return extractResponse[string](response)
  60  }
  61  
  62  func (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) {
  63  	payload := ClientIDRequest{
  64  		SessionID: sessionID,
  65  		SysUserID: sysUserID,
  66  	}
  67  
  68  	endpoint, err := url.Parse(c.serverURL)
  69  	if err != nil {
  70  		return 0, err
  71  	}
  72  
  73  	endpoint.RawQuery = "client_get_id"
  74  
  75  	req, err := newJSONRequest(ctx, endpoint, payload)
  76  	if err != nil {
  77  		return 0, err
  78  	}
  79  
  80  	var response APIResponse
  81  
  82  	err = c.do(req, &response)
  83  	if err != nil {
  84  		return 0, err
  85  	}
  86  
  87  	return extractResponse[int](response)
  88  }
  89  
  90  // GetZoneID returns the zone ID for the given name.
  91  func (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) {
  92  	payload := map[string]any{
  93  		"session_id": sessionID,
  94  		"origin":     name,
  95  	}
  96  
  97  	endpoint, err := url.Parse(c.serverURL)
  98  	if err != nil {
  99  		return 0, err
 100  	}
 101  
 102  	endpoint.RawQuery = "dns_zone_get_id"
 103  
 104  	req, err := newJSONRequest(ctx, endpoint, payload)
 105  	if err != nil {
 106  		return 0, err
 107  	}
 108  
 109  	var response APIResponse
 110  
 111  	err = c.do(req, &response)
 112  	if err != nil {
 113  		return 0, err
 114  	}
 115  
 116  	return extractResponse[int](response)
 117  }
 118  
 119  // GetZone returns the zone information for the zone ID.
 120  func (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) {
 121  	payload := map[string]any{
 122  		"session_id": sessionID,
 123  		"primary_id": zoneID,
 124  	}
 125  
 126  	endpoint, err := url.Parse(c.serverURL)
 127  	if err != nil {
 128  		return nil, err
 129  	}
 130  
 131  	endpoint.RawQuery = "dns_zone_get"
 132  
 133  	req, err := newJSONRequest(ctx, endpoint, payload)
 134  	if err != nil {
 135  		return nil, err
 136  	}
 137  
 138  	var response APIResponse
 139  
 140  	err = c.do(req, &response)
 141  	if err != nil {
 142  		return nil, err
 143  	}
 144  
 145  	return extractResponse[*Zone](response)
 146  }
 147  
 148  // GetTXT returns the TXT record for the given name.
 149  // `name` must be a fully qualified domain name, e.g. "example.com.".
 150  func (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) {
 151  	payload := GetTXTRequest{
 152  		SessionID: sessionID,
 153  		PrimaryID: struct {
 154  			Name string `json:"name"`
 155  			Type string `json:"type"`
 156  		}{
 157  			Name: name,
 158  			Type: "txt",
 159  		},
 160  	}
 161  
 162  	endpoint, err := url.Parse(c.serverURL)
 163  	if err != nil {
 164  		return nil, err
 165  	}
 166  
 167  	endpoint.RawQuery = "dns_txt_get"
 168  
 169  	req, err := newJSONRequest(ctx, endpoint, payload)
 170  	if err != nil {
 171  		return nil, err
 172  	}
 173  
 174  	var response APIResponse
 175  
 176  	err = c.do(req, &response)
 177  	if err != nil {
 178  		return nil, err
 179  	}
 180  
 181  	return extractResponse[*Record](response)
 182  }
 183  
 184  // AddTXT adds a TXT record.
 185  // It returns the ID of the newly created record.
 186  func (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) {
 187  	payload := AddTXTRequest{
 188  		SessionID:    sessionID,
 189  		ClientID:     clientID,
 190  		Params:       &params,
 191  		UpdateSerial: true,
 192  	}
 193  
 194  	endpoint, err := url.Parse(c.serverURL)
 195  	if err != nil {
 196  		return "", err
 197  	}
 198  
 199  	endpoint.RawQuery = "dns_txt_add"
 200  
 201  	req, err := newJSONRequest(ctx, endpoint, payload)
 202  	if err != nil {
 203  		return "", err
 204  	}
 205  
 206  	var response APIResponse
 207  
 208  	err = c.do(req, &response)
 209  	if err != nil {
 210  		return "", err
 211  	}
 212  
 213  	return extractResponse[string](response)
 214  }
 215  
 216  // DeleteTXT deletes a TXT record.
 217  // It returns the number of deleted records.
 218  func (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) {
 219  	payload := DeleteTXTRequest{
 220  		SessionID:    sessionID,
 221  		PrimaryID:    recordID,
 222  		UpdateSerial: true,
 223  	}
 224  
 225  	endpoint, err := url.Parse(c.serverURL)
 226  	if err != nil {
 227  		return 0, err
 228  	}
 229  
 230  	endpoint.RawQuery = "dns_txt_delete"
 231  
 232  	req, err := newJSONRequest(ctx, endpoint, payload)
 233  	if err != nil {
 234  		return 0, err
 235  	}
 236  
 237  	var response APIResponse
 238  
 239  	err = c.do(req, &response)
 240  	if err != nil {
 241  		return 0, err
 242  	}
 243  
 244  	return extractResponse[int](response)
 245  }
 246  
 247  func (c *Client) do(req *http.Request, result any) error {
 248  	resp, err := c.HTTPClient.Do(req)
 249  	if err != nil {
 250  		return errutils.NewHTTPDoError(req, err)
 251  	}
 252  
 253  	defer func() { _ = resp.Body.Close() }()
 254  
 255  	if resp.StatusCode/100 != 2 {
 256  		raw, _ := io.ReadAll(resp.Body)
 257  
 258  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 259  	}
 260  
 261  	if result == nil {
 262  		return nil
 263  	}
 264  
 265  	raw, err := io.ReadAll(resp.Body)
 266  	if err != nil {
 267  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 268  	}
 269  
 270  	err = json.Unmarshal(raw, result)
 271  	if err != nil {
 272  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 273  	}
 274  
 275  	return nil
 276  }
 277  
 278  func newJSONRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {
 279  	buf := new(bytes.Buffer)
 280  
 281  	if payload != nil {
 282  		err := json.NewEncoder(buf).Encode(payload)
 283  		if err != nil {
 284  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 285  		}
 286  	}
 287  
 288  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), buf)
 289  	if err != nil {
 290  		return nil, fmt.Errorf("unable to create request: %w", err)
 291  	}
 292  
 293  	req.Header.Set("Accept", "application/json")
 294  
 295  	if payload != nil {
 296  		req.Header.Set("Content-Type", "application/json")
 297  	}
 298  
 299  	return req, nil
 300  }
 301  
 302  func extractResponse[T any](response APIResponse) (T, error) {
 303  	if response.Code != "ok" {
 304  		var zero T
 305  
 306  		return zero, &APIError{APIResponse: response}
 307  	}
 308  
 309  	var result T
 310  
 311  	err := json.Unmarshal(response.Response, &result)
 312  	if err != nil {
 313  		var zero T
 314  		return zero, fmt.Errorf("unable to unmarshal response: %s, %w", string(response.Response), err)
 315  	}
 316  
 317  	return result, nil
 318  }
 319