client.go raw

   1  package internal
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"net/url"
  11  	"strconv"
  12  	"strings"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  16  	querystring "github.com/google/go-querystring/query"
  17  )
  18  
  19  const defaultBaseURL = "https://api.panel.octenium.com/"
  20  
  21  const statusSuccess = "success"
  22  
  23  // Client the Octenium API client.
  24  type Client struct {
  25  	apiKey string
  26  
  27  	BaseURL    *url.URL
  28  	HTTPClient *http.Client
  29  }
  30  
  31  // NewClient creates a new Client.
  32  func NewClient(apiKey string) (*Client, error) {
  33  	if apiKey == "" {
  34  		return nil, errors.New("credentials missing")
  35  	}
  36  
  37  	baseURL, _ := url.Parse(defaultBaseURL)
  38  
  39  	return &Client{
  40  		apiKey:     apiKey,
  41  		BaseURL:    baseURL,
  42  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  43  	}, nil
  44  }
  45  
  46  // ListDomains retrieves a list of domains.
  47  // https://octenium.com/api#tag/Domains/operation/listdomains
  48  func (c *Client) ListDomains(ctx context.Context, domain string) (map[string]Domain, error) {
  49  	endpoint := c.BaseURL.JoinPath("domains")
  50  
  51  	data := endpoint.Query()
  52  	data.Set("domain-name", domain)
  53  	endpoint.RawQuery = data.Encode()
  54  
  55  	req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
  56  	if err != nil {
  57  		return nil, err
  58  	}
  59  
  60  	result := &DomainsResponse{}
  61  
  62  	err = c.do(req, result)
  63  	if err != nil {
  64  		return nil, err
  65  	}
  66  
  67  	return result.Domains, nil
  68  }
  69  
  70  // ListDNSRecords retrieves a list of DNS records.
  71  // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-list
  72  func (c *Client) ListDNSRecords(ctx context.Context, orderID, recordType string) ([]Record, error) {
  73  	endpoint := c.BaseURL.JoinPath("domains", "dns-records", "list")
  74  
  75  	data := make(url.Values)
  76  	data.Set("order-id", orderID)
  77  	data.Set("types[]", recordType)
  78  
  79  	req, err := newRequest(ctx, http.MethodPost, endpoint, data)
  80  	if err != nil {
  81  		return nil, err
  82  	}
  83  
  84  	result := &ListRecordsResponse{}
  85  
  86  	err = c.do(req, result)
  87  	if err != nil {
  88  		return nil, err
  89  	}
  90  
  91  	return result.Records, nil
  92  }
  93  
  94  // AddDNSRecord adds a DNS record.
  95  // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-add
  96  func (c *Client) AddDNSRecord(ctx context.Context, orderID string, record Record) (*Record, error) {
  97  	endpoint := c.BaseURL.JoinPath("domains", "dns-records", "add")
  98  
  99  	data, err := querystring.Values(record)
 100  	if err != nil {
 101  		return nil, err
 102  	}
 103  
 104  	data.Set("order-id", orderID)
 105  
 106  	req, err := newRequest(ctx, http.MethodPost, endpoint, data)
 107  	if err != nil {
 108  		return nil, err
 109  	}
 110  
 111  	result := &AddRecordResponse{}
 112  
 113  	err = c.do(req, result)
 114  	if err != nil {
 115  		return nil, err
 116  	}
 117  
 118  	return result.Record, nil
 119  }
 120  
 121  // DeleteDNSRecord deletes a DNS record.
 122  // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-delete
 123  func (c *Client) DeleteDNSRecord(ctx context.Context, orderID string, recordID int) (*DeletedRecordInfo, error) {
 124  	endpoint := c.BaseURL.JoinPath("domains", "dns-records", "delete")
 125  
 126  	data := make(url.Values)
 127  	data.Set("order-id", orderID)
 128  	data.Set("line", strconv.Itoa(recordID))
 129  
 130  	req, err := newRequest(ctx, http.MethodPost, endpoint, data)
 131  	if err != nil {
 132  		return nil, err
 133  	}
 134  
 135  	result := &DeleteRecordResponse{}
 136  
 137  	err = c.do(req, result)
 138  	if err != nil {
 139  		return nil, err
 140  	}
 141  
 142  	return result.Deleted, nil
 143  }
 144  
 145  func (c *Client) do(req *http.Request, result any) error {
 146  	req.Header.Set("X-Api-Key", c.apiKey)
 147  
 148  	resp, err := c.HTTPClient.Do(req)
 149  	if err != nil {
 150  		return errutils.NewHTTPDoError(req, err)
 151  	}
 152  
 153  	defer func() { _ = resp.Body.Close() }()
 154  
 155  	if resp.StatusCode/100 != 2 {
 156  		raw, _ := io.ReadAll(resp.Body)
 157  
 158  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 159  	}
 160  
 161  	raw, err := io.ReadAll(resp.Body)
 162  	if err != nil {
 163  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 164  	}
 165  
 166  	var response APIResponse
 167  
 168  	err = json.Unmarshal(raw, &response)
 169  	if err != nil {
 170  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 171  	}
 172  
 173  	if response.Status != statusSuccess {
 174  		return fmt.Errorf("unexpected status: %s: %s", response.Status, response.Error)
 175  	}
 176  
 177  	err = json.Unmarshal(response.Response, result)
 178  	if err != nil {
 179  		return errutils.NewUnmarshalError(req, resp.StatusCode, response.Response, err)
 180  	}
 181  
 182  	return nil
 183  }
 184  
 185  func newRequest(ctx context.Context, method string, endpoint *url.URL, payload url.Values) (*http.Request, error) {
 186  	var body io.Reader = http.NoBody
 187  
 188  	if method == http.MethodPost && payload != nil {
 189  		body = strings.NewReader(payload.Encode())
 190  	}
 191  
 192  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
 193  	if err != nil {
 194  		return nil, fmt.Errorf("unable to create request: %w", err)
 195  	}
 196  
 197  	req.Header.Set("Accept", "application/json")
 198  
 199  	if method == http.MethodPost && payload != nil {
 200  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 201  	}
 202  
 203  	return req, nil
 204  }
 205