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 "sync"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 type Client struct {
19 username string
20 password string
21 domainName string
22 projectName string
23
24 IdentityEndpoint string
25 token string
26 muToken sync.Mutex
27
28 baseURL *url.URL
29 muBaseURL sync.Mutex
30
31 HTTPClient *http.Client
32 }
33
34 func NewClient(username, password, domainName, projectName string) *Client {
35 return &Client{
36 username: username,
37 password: password,
38 domainName: domainName,
39 projectName: projectName,
40 IdentityEndpoint: DefaultIdentityEndpoint,
41 HTTPClient: &http.Client{Timeout: 5 * time.Second},
42 }
43 }
44
45 func (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) (string, error) {
46 zonesResp, err := c.getZones(ctx, zone, privateZone)
47 if err != nil {
48 return "", err
49 }
50
51 if len(zonesResp.Zones) < 1 {
52 return "", fmt.Errorf("zone %s not found", zone)
53 }
54
55 for _, z := range zonesResp.Zones {
56 if z.Name == zone {
57 return z.ID, nil
58 }
59 }
60
61 return "", fmt.Errorf("zone %s not found", zone)
62 }
63
64 // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/public_zone_management/querying_public_zones.html
65 func (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (*ZonesResponse, error) {
66 c.muBaseURL.Lock()
67 endpoint := c.baseURL.JoinPath("zones")
68 c.muBaseURL.Unlock()
69
70 query := endpoint.Query()
71 query.Set("name", zone)
72
73 if privateZone {
74 query.Set("type", "private")
75 }
76
77 endpoint.RawQuery = query.Encode()
78
79 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
80 if err != nil {
81 return nil, err
82 }
83
84 var zones ZonesResponse
85
86 err = c.do(req, &zones)
87 if err != nil {
88 return nil, err
89 }
90
91 return &zones, nil
92 }
93
94 func (c *Client) GetRecordSetID(ctx context.Context, zoneID, fqdn string) (string, error) {
95 recordSetsRes, err := c.getRecordSet(ctx, zoneID, fqdn)
96 if err != nil {
97 return "", err
98 }
99
100 if len(recordSetsRes.RecordSets) < 1 {
101 return "", errors.New("record not found")
102 }
103
104 if len(recordSetsRes.RecordSets) > 1 {
105 return "", errors.New("to many records found")
106 }
107
108 if recordSetsRes.RecordSets[0].ID == "" {
109 return "", errors.New("id not found")
110 }
111
112 return recordSetsRes.RecordSets[0].ID, nil
113 }
114
115 // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/querying_all_record_sets.html
116 func (c *Client) getRecordSet(ctx context.Context, zoneID, fqdn string) (*RecordSetsResponse, error) {
117 c.muBaseURL.Lock()
118 endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets")
119 c.muBaseURL.Unlock()
120
121 query := endpoint.Query()
122 query.Set("type", "TXT")
123 query.Set("name", fqdn)
124 endpoint.RawQuery = query.Encode()
125
126 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
127 if err != nil {
128 return nil, err
129 }
130
131 var recordSetsRes RecordSetsResponse
132
133 err = c.do(req, &recordSetsRes)
134 if err != nil {
135 return nil, err
136 }
137
138 return &recordSetsRes, nil
139 }
140
141 // CreateRecordSet creates a record.
142 // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/creating_a_record_set.html
143 func (c *Client) CreateRecordSet(ctx context.Context, zoneID string, record RecordSets) error {
144 c.muBaseURL.Lock()
145 endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets")
146 c.muBaseURL.Unlock()
147
148 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
149 if err != nil {
150 return err
151 }
152
153 return c.do(req, nil)
154 }
155
156 // DeleteRecordSet delete a record set.
157 // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/deleting_a_record_set.html
158 func (c *Client) DeleteRecordSet(ctx context.Context, zoneID, recordID string) error {
159 c.muBaseURL.Lock()
160 endpoint := c.baseURL.JoinPath("zones", zoneID, "recordsets", recordID)
161 c.muBaseURL.Unlock()
162
163 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
164 if err != nil {
165 return err
166 }
167
168 return c.do(req, nil)
169 }
170
171 func (c *Client) do(req *http.Request, result any) error {
172 c.muToken.Lock()
173
174 if c.token != "" {
175 req.Header.Set("X-Auth-Token", c.token)
176 }
177
178 c.muToken.Unlock()
179
180 resp, err := c.HTTPClient.Do(req)
181 if err != nil {
182 return errutils.NewHTTPDoError(req, err)
183 }
184
185 defer func() { _ = resp.Body.Close() }()
186
187 if resp.StatusCode >= http.StatusBadRequest {
188 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
189 }
190
191 if result == nil {
192 return nil
193 }
194
195 raw, err := io.ReadAll(resp.Body)
196 if err != nil {
197 return errutils.NewReadResponseError(req, resp.StatusCode, err)
198 }
199
200 err = json.Unmarshal(raw, result)
201 if err != nil {
202 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
203 }
204
205 return nil
206 }
207
208 func newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) {
209 buf := new(bytes.Buffer)
210
211 if payload != nil {
212 err := json.NewEncoder(buf).Encode(payload)
213 if err != nil {
214 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
215 }
216 }
217
218 req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s", endpoint), buf)
219 if err != nil {
220 return nil, fmt.Errorf("unable to create request: %w", err)
221 }
222
223 req.Header.Set("Accept", "application/json")
224
225 if payload != nil {
226 req.Header.Set("Content-Type", "application/json")
227 }
228
229 return req, nil
230 }
231