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