client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const dnsServiceBaseURL = "https://dns-service.%s.conoha.io"
  18  
  19  // Client is a ConoHa API client.
  20  type Client struct {
  21  	token string
  22  
  23  	baseURL    *url.URL
  24  	HTTPClient *http.Client
  25  }
  26  
  27  // NewClient returns a client instance logged into the ConoHa service.
  28  func NewClient(region, token string) (*Client, error) {
  29  	baseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region))
  30  	if err != nil {
  31  		return nil, err
  32  	}
  33  
  34  	return &Client{
  35  		token:      token,
  36  		baseURL:    baseURL,
  37  		HTTPClient: &http.Client{Timeout: 5 * time.Second},
  38  	}, nil
  39  }
  40  
  41  // GetDomainID returns an ID of specified domain.
  42  func (c *Client) GetDomainID(ctx context.Context, domainName string) (string, error) {
  43  	domainList, err := c.getDomains(ctx)
  44  	if err != nil {
  45  		return "", err
  46  	}
  47  
  48  	for _, domain := range domainList.Domains {
  49  		if domain.Name == domainName {
  50  			return domain.ID, nil
  51  		}
  52  	}
  53  
  54  	return "", fmt.Errorf("no such domain: %s", domainName)
  55  }
  56  
  57  // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-domains-v2/?btn_id=reference-api-vps2--sidebar_reference-paas-dns-list-domains-v2
  58  func (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) {
  59  	endpoint := c.baseURL.JoinPath("v1", "domains")
  60  
  61  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  62  	if err != nil {
  63  		return nil, err
  64  	}
  65  
  66  	domainList := &DomainListResponse{}
  67  
  68  	err = c.do(req, domainList)
  69  	if err != nil {
  70  		return nil, err
  71  	}
  72  
  73  	return domainList, nil
  74  }
  75  
  76  // GetRecordID returns an ID of specified record.
  77  func (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordType, data string) (string, error) {
  78  	recordList, err := c.getRecords(ctx, domainID)
  79  	if err != nil {
  80  		return "", err
  81  	}
  82  
  83  	for _, record := range recordList.Records {
  84  		if record.Name == recordName && record.Type == recordType && record.Data == data {
  85  			return record.ID, nil
  86  		}
  87  	}
  88  
  89  	return "", errors.New("no such record")
  90  }
  91  
  92  // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-records-in-a-domain-v2/?btn_id=reference-paas-dns-list-domains-v2--sidebar_reference-paas-dns-list-records-in-a-domain-v2
  93  func (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) {
  94  	endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records")
  95  
  96  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  97  	if err != nil {
  98  		return nil, err
  99  	}
 100  
 101  	recordList := &RecordListResponse{}
 102  
 103  	err = c.do(req, recordList)
 104  	if err != nil {
 105  		return nil, err
 106  	}
 107  
 108  	return recordList, nil
 109  }
 110  
 111  // CreateRecord adds new record.
 112  func (c *Client) CreateRecord(ctx context.Context, domainID string, record Record) error {
 113  	_, err := c.createRecord(ctx, domainID, record)
 114  	return err
 115  }
 116  
 117  // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-create-record-v2/?btn_id=reference-paas-dns-list-records-in-a-domain-v2--sidebar_reference-paas-dns-create-record-v2
 118  func (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) {
 119  	endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records")
 120  
 121  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
 122  	if err != nil {
 123  		return nil, err
 124  	}
 125  
 126  	newRecord := &Record{}
 127  
 128  	err = c.do(req, newRecord)
 129  	if err != nil {
 130  		return nil, err
 131  	}
 132  
 133  	return newRecord, nil
 134  }
 135  
 136  // DeleteRecord removes specified record.
 137  // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-delete-a-record-v2/?btn_id=reference-paas-dns-create-record-v2--sidebar_reference-paas-dns-delete-a-record-v2
 138  func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error {
 139  	endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records", recordID)
 140  
 141  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 142  	if err != nil {
 143  		return err
 144  	}
 145  
 146  	return c.do(req, nil)
 147  }
 148  
 149  func (c *Client) do(req *http.Request, result any) error {
 150  	if c.token != "" {
 151  		req.Header.Set("X-Auth-Token", c.token)
 152  	}
 153  
 154  	resp, err := c.HTTPClient.Do(req)
 155  	if err != nil {
 156  		return errutils.NewHTTPDoError(req, err)
 157  	}
 158  
 159  	defer func() { _ = resp.Body.Close() }()
 160  
 161  	if resp.StatusCode != http.StatusOK {
 162  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 163  	}
 164  
 165  	if result == nil {
 166  		return nil
 167  	}
 168  
 169  	raw, err := io.ReadAll(resp.Body)
 170  	if err != nil {
 171  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 172  	}
 173  
 174  	err = json.Unmarshal(raw, result)
 175  	if err != nil {
 176  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 177  	}
 178  
 179  	return nil
 180  }
 181  
 182  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 183  	buf := new(bytes.Buffer)
 184  
 185  	if payload != nil {
 186  		err := json.NewEncoder(buf).Encode(payload)
 187  		if err != nil {
 188  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 189  		}
 190  	}
 191  
 192  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 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 payload != nil {
 200  		req.Header.Set("Content-Type", "application/json")
 201  	}
 202  
 203  	return req, nil
 204  }
 205