client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  13  )
  14  
  15  const apiEndpoint = "https://njal.la/api/1/"
  16  
  17  const authorizationHeader = "Authorization"
  18  
  19  // Client is a Njalla API client.
  20  type Client struct {
  21  	token string
  22  
  23  	apiEndpoint string
  24  	HTTPClient  *http.Client
  25  }
  26  
  27  // NewClient creates a new Client.
  28  func NewClient(token string) *Client {
  29  	return &Client{
  30  		token:       token,
  31  		apiEndpoint: apiEndpoint,
  32  		HTTPClient:  &http.Client{Timeout: 5 * time.Second},
  33  	}
  34  }
  35  
  36  // AddRecord adds a record.
  37  func (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {
  38  	data := APIRequest{
  39  		Method: "add-record",
  40  		Params: record,
  41  	}
  42  
  43  	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
  44  	if err != nil {
  45  		return nil, err
  46  	}
  47  
  48  	var result APIResponse[*Record]
  49  
  50  	err = c.do(req, &result)
  51  	if err != nil {
  52  		return nil, err
  53  	}
  54  
  55  	return result.Result, nil
  56  }
  57  
  58  // RemoveRecord removes a record.
  59  func (c *Client) RemoveRecord(ctx context.Context, id, domain string) error {
  60  	data := APIRequest{
  61  		Method: "remove-record",
  62  		Params: Record{
  63  			ID:     id,
  64  			Domain: domain,
  65  		},
  66  	}
  67  
  68  	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
  69  	if err != nil {
  70  		return err
  71  	}
  72  
  73  	err = c.do(req, &APIResponse[json.RawMessage]{})
  74  	if err != nil {
  75  		return err
  76  	}
  77  
  78  	return nil
  79  }
  80  
  81  // ListRecords list the records for one domain.
  82  func (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, error) {
  83  	data := APIRequest{
  84  		Method: "list-records",
  85  		Params: Record{
  86  			Domain: domain,
  87  		},
  88  	}
  89  
  90  	req, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)
  91  	if err != nil {
  92  		return nil, err
  93  	}
  94  
  95  	var result APIResponse[Records]
  96  
  97  	err = c.do(req, &result)
  98  	if err != nil {
  99  		return nil, err
 100  	}
 101  
 102  	return result.Result.Records, nil
 103  }
 104  
 105  func (c *Client) do(req *http.Request, result Response) error {
 106  	req.Header.Set(authorizationHeader, "Njalla "+c.token)
 107  
 108  	resp, err := c.HTTPClient.Do(req)
 109  	if err != nil {
 110  		return errutils.NewHTTPDoError(req, err)
 111  	}
 112  
 113  	defer func() { _ = resp.Body.Close() }()
 114  
 115  	if resp.StatusCode != http.StatusOK {
 116  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 117  	}
 118  
 119  	raw, err := io.ReadAll(resp.Body)
 120  	if err != nil {
 121  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 122  	}
 123  
 124  	err = json.Unmarshal(raw, result)
 125  	if err != nil {
 126  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 127  	}
 128  
 129  	return result.GetError()
 130  }
 131  
 132  func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {
 133  	buf := new(bytes.Buffer)
 134  
 135  	if payload != nil {
 136  		err := json.NewEncoder(buf).Encode(payload)
 137  		if err != nil {
 138  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 139  		}
 140  	}
 141  
 142  	req, err := http.NewRequestWithContext(ctx, method, endpoint, buf)
 143  	if err != nil {
 144  		return nil, fmt.Errorf("unable to create request: %w", err)
 145  	}
 146  
 147  	req.Header.Set("Accept", "application/json")
 148  
 149  	if payload != nil {
 150  		req.Header.Set("Content-Type", "application/json")
 151  	}
 152  
 153  	return req, nil
 154  }
 155