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 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
16 )
17
18 const defaultBaseURL = "https://dns.de-fra.ionos.com"
19
20 const authorizationHeader = "Authorization"
21
22 // Client the Ionos Cloud API client.
23 type Client struct {
24 apiKey string
25
26 BaseURL *url.URL
27 HTTPClient *http.Client
28 }
29
30 // NewClient creates a new Client.
31 func NewClient(apiKey string) (*Client, error) {
32 if apiKey == "" {
33 return nil, errors.New("credentials missing")
34 }
35
36 baseURL, _ := url.Parse(defaultBaseURL)
37
38 return &Client{
39 apiKey: apiKey,
40 BaseURL: baseURL,
41 HTTPClient: &http.Client{Timeout: 10 * time.Second},
42 }, nil
43 }
44
45 // RetrieveZones returns a list of the DNS zones.
46 // https://api.ionos.com/docs/dns/v1/#tag/Zones/operation/zonesGet
47 func (c *Client) RetrieveZones(ctx context.Context, zoneName string) ([]Zone, error) {
48 endpoint := c.BaseURL.JoinPath("zones")
49
50 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
51 if err != nil {
52 return nil, err
53 }
54
55 query := req.URL.Query()
56 query.Add("filter.zoneName", zoneName)
57 req.URL.RawQuery = query.Encode()
58
59 result := ZonesResponse{}
60
61 if err := c.do(req, &result); err != nil {
62 return nil, err
63 }
64
65 return result.Items, nil
66 }
67
68 // CreateRecord creates a new record for the DNS zone.
69 // https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsPost
70 func (c *Client) CreateRecord(ctx context.Context, zoneID string, record RecordProperties) (*RecordResponse, error) {
71 endpoint := c.BaseURL.JoinPath("zones", zoneID, "records")
72
73 payload := map[string]RecordProperties{
74 "properties": record,
75 }
76
77 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)
78 if err != nil {
79 return nil, err
80 }
81
82 result := &RecordResponse{}
83
84 if err := c.do(req, result); err != nil {
85 return nil, err
86 }
87
88 return result, nil
89 }
90
91 // DeleteRecord deletes a specified record from the DNS zone.
92 // https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsDelete
93 func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {
94 endpoint := c.BaseURL.JoinPath("zones", zoneID, "records", recordID)
95
96 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
97 if err != nil {
98 return err
99 }
100
101 return c.do(req, nil)
102 }
103
104 func (c *Client) do(req *http.Request, result any) error {
105 useragent.SetHeader(req.Header)
106
107 req.Header.Set(authorizationHeader, "Bearer "+c.apiKey)
108
109 resp, err := c.HTTPClient.Do(req)
110 if err != nil {
111 return errutils.NewHTTPDoError(req, err)
112 }
113
114 defer func() { _ = resp.Body.Close() }()
115
116 if resp.StatusCode/100 != 2 {
117 return parseError(req, resp)
118 }
119
120 if result == nil {
121 return nil
122 }
123
124 raw, err := io.ReadAll(resp.Body)
125 if err != nil {
126 return errutils.NewReadResponseError(req, resp.StatusCode, err)
127 }
128
129 err = json.Unmarshal(raw, result)
130 if err != nil {
131 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
132 }
133
134 return nil
135 }
136
137 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
138 buf := new(bytes.Buffer)
139
140 if payload != nil {
141 err := json.NewEncoder(buf).Encode(payload)
142 if err != nil {
143 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
144 }
145 }
146
147 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
148 if err != nil {
149 return nil, fmt.Errorf("unable to create request: %w", err)
150 }
151
152 req.Header.Set("Accept", "application/json")
153
154 if payload != nil {
155 req.Header.Set("Content-Type", "application/json")
156 }
157
158 return req, nil
159 }
160
161 func parseError(req *http.Request, resp *http.Response) error {
162 raw, _ := io.ReadAll(resp.Body)
163
164 var errAPI APIError
165
166 err := json.Unmarshal(raw, &errAPI)
167 if err != nil {
168 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
169 }
170
171 return &errAPI
172 }
173