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  	"strconv"
  12  	"strings"
  13  	"time"
  14  
  15  	"github.com/go-acme/lego/v4/challenge/dns01"
  16  	"github.com/go-acme/lego/v4/log"
  17  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  18  	"golang.org/x/oauth2"
  19  )
  20  
  21  // DefaultBaseURL Default API endpoint.
  22  const DefaultBaseURL = "https://api.infomaniak.com"
  23  
  24  // Client the Infomaniak client.
  25  type Client struct {
  26  	baseURL    *url.URL
  27  	httpClient *http.Client
  28  }
  29  
  30  // New Creates a new Infomaniak client.
  31  func New(hc *http.Client, apiEndpoint string) (*Client, error) {
  32  	baseURL, err := url.Parse(apiEndpoint)
  33  	if err != nil {
  34  		return nil, err
  35  	}
  36  
  37  	if hc == nil {
  38  		hc = &http.Client{Timeout: 5 * time.Second}
  39  	}
  40  
  41  	return &Client{baseURL: baseURL, httpClient: hc}, nil
  42  }
  43  
  44  func (c *Client) CreateDNSRecord(ctx context.Context, domain *DNSDomain, record Record) (string, error) {
  45  	endpoint := c.baseURL.JoinPath("1", "domain", strconv.FormatUint(domain.ID, 10), "dns", "record")
  46  
  47  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
  48  	if err != nil {
  49  		return "", fmt.Errorf("failed to create request: %w", err)
  50  	}
  51  
  52  	result := APIResponse[string]{}
  53  
  54  	err = c.do(req, &result)
  55  	if err != nil {
  56  		return "", err
  57  	}
  58  
  59  	return result.Data, err
  60  }
  61  
  62  func (c *Client) DeleteDNSRecord(ctx context.Context, domainID uint64, recordID string) error {
  63  	endpoint := c.baseURL.JoinPath("1", "domain", strconv.FormatUint(domainID, 10), "dns", "record", recordID)
  64  
  65  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
  66  	if err != nil {
  67  		return fmt.Errorf("failed to create request: %w", err)
  68  	}
  69  
  70  	return c.do(req, &APIResponse[json.RawMessage]{})
  71  }
  72  
  73  // GetDomainByName gets a Domain object from its name.
  74  func (c *Client) GetDomainByName(ctx context.Context, name string) (*DNSDomain, error) {
  75  	name = dns01.UnFqdn(name)
  76  
  77  	// Try to find the most specific domain
  78  	// starts with the FQDN, then remove each left label until we have a match
  79  	for {
  80  		i := strings.Index(name, ".")
  81  		if i == -1 {
  82  			break
  83  		}
  84  
  85  		domain, err := c.getDomainByName(ctx, name)
  86  		if err != nil {
  87  			return nil, err
  88  		}
  89  
  90  		if domain != nil {
  91  			return domain, nil
  92  		}
  93  
  94  		log.Infof("domain %q not found, trying with %q", name, name[i+1:])
  95  
  96  		name = name[i+1:]
  97  	}
  98  
  99  	return nil, fmt.Errorf("domain not found %s", name)
 100  }
 101  
 102  func (c *Client) getDomainByName(ctx context.Context, name string) (*DNSDomain, error) {
 103  	endpoint := c.baseURL.JoinPath("1", "product")
 104  
 105  	query := endpoint.Query()
 106  	query.Add("service_name", "domain")
 107  	query.Add("customer_name", name)
 108  	endpoint.RawQuery = query.Encode()
 109  
 110  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
 111  	if err != nil {
 112  		return nil, err
 113  	}
 114  
 115  	result := APIResponse[[]DNSDomain]{}
 116  
 117  	err = c.do(req, &result)
 118  	if err != nil {
 119  		return nil, err
 120  	}
 121  
 122  	for _, domain := range result.Data {
 123  		if domain.CustomerName == name {
 124  			return &domain, nil
 125  		}
 126  	}
 127  
 128  	return nil, nil
 129  }
 130  
 131  func (c *Client) do(req *http.Request, result Response) error {
 132  	resp, err := c.httpClient.Do(req)
 133  	if err != nil {
 134  		return errutils.NewHTTPDoError(req, err)
 135  	}
 136  
 137  	defer func() { _ = resp.Body.Close() }()
 138  
 139  	raw, err := io.ReadAll(resp.Body)
 140  	if err != nil {
 141  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 142  	}
 143  
 144  	if err := json.Unmarshal(raw, result); err != nil {
 145  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 146  	}
 147  
 148  	if result.GetResult() != "success" {
 149  		return fmt.Errorf("%d: unexpected API result (%s): %w", resp.StatusCode, result.GetResult(), result.GetError())
 150  	}
 151  
 152  	return nil
 153  }
 154  
 155  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 156  	buf := new(bytes.Buffer)
 157  
 158  	if payload != nil {
 159  		err := json.NewEncoder(buf).Encode(payload)
 160  		if err != nil {
 161  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 162  		}
 163  	}
 164  
 165  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 166  	if err != nil {
 167  		return nil, fmt.Errorf("unable to create request: %w", err)
 168  	}
 169  
 170  	req.Header.Set("Accept", "application/json")
 171  
 172  	if payload != nil {
 173  		req.Header.Set("Content-Type", "application/json")
 174  	}
 175  
 176  	return req, nil
 177  }
 178  
 179  func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
 180  	if client == nil {
 181  		client = &http.Client{Timeout: 5 * time.Second}
 182  	}
 183  
 184  	client.Transport = &oauth2.Transport{
 185  		Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
 186  		Base:   client.Transport,
 187  	}
 188  
 189  	return client
 190  }
 191