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 "strconv"
13 "strings"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 const defaultBaseURL = "https://clouddns.manageengine.com/v1"
19
20 // Client the ManageEngine CloudDNS API client.
21 type Client struct {
22 baseURL *url.URL
23 httpClient *http.Client
24 }
25
26 // NewClient creates a new Client.
27 func NewClient(hc *http.Client) *Client {
28 baseURL, _ := url.Parse(defaultBaseURL)
29
30 return &Client{
31 baseURL: baseURL,
32 httpClient: hc,
33 }
34 }
35
36 // GetAllZones gets all zones.
37 // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All
38 func (c *Client) GetAllZones(ctx context.Context) ([]Zone, error) {
39 endpoint := c.baseURL.JoinPath("dns", "domain")
40
41 req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
42 if err != nil {
43 return nil, err
44 }
45
46 var results []Zone
47
48 err = c.do(req, &results)
49 if err != nil {
50 return nil, err
51 }
52
53 return results, nil
54 }
55
56 // GetAllZoneRecords gets all "zone records" for a zone.
57 // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All_9
58 func (c *Client) GetAllZoneRecords(ctx context.Context, zoneID int) ([]ZoneRecord, error) {
59 endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT")
60
61 req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
62 if err != nil {
63 return nil, err
64 }
65
66 var results []ZoneRecord
67
68 err = c.do(req, &results)
69 if err != nil {
70 return nil, err
71 }
72
73 return results, nil
74 }
75
76 // DeleteZoneRecord deletes a "zone record".
77 // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#DEL_Delete_10
78 func (c *Client) DeleteZoneRecord(ctx context.Context, zoneID, domainID int) error {
79 endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", strconv.Itoa(domainID))
80
81 req, err := newRequest(ctx, http.MethodDelete, endpoint, nil)
82 if err != nil {
83 return err
84 }
85
86 var results APIResponse
87
88 return c.do(req, &results)
89 }
90
91 // CreateZoneRecord creates a "zone record".
92 // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#POST_Create_10
93 func (c *Client) CreateZoneRecord(ctx context.Context, zoneID int, record ZoneRecord) error {
94 endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(zoneID), "records", "SPF_TXT", "/")
95
96 req, err := newRequest(ctx, http.MethodPost, endpoint, []ZoneRecord{record})
97 if err != nil {
98 return err
99 }
100
101 var results APIResponse
102
103 return c.do(req, &results)
104 }
105
106 // UpdateZoneRecord update an existing "zone record".
107 // https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#PUT_Update_10
108 func (c *Client) UpdateZoneRecord(ctx context.Context, record ZoneRecord) error {
109 if record.SpfTxtDomainID == 0 {
110 return errors.New("SpfTxtDomainID is empty")
111 }
112
113 if record.ZoneID == 0 {
114 return errors.New("ZoneID is empty")
115 }
116
117 endpoint := c.baseURL.JoinPath("dns", "domain", strconv.Itoa(record.ZoneID), "records", "SPF_TXT", strconv.Itoa(record.SpfTxtDomainID), "/")
118
119 req, err := newRequest(ctx, http.MethodPut, endpoint, []ZoneRecord{record})
120 if err != nil {
121 return err
122 }
123
124 var results APIResponse
125
126 return c.do(req, &results)
127 }
128
129 func (c *Client) do(req *http.Request, result any) error {
130 resp, err := c.httpClient.Do(req)
131 if err != nil {
132 return errutils.NewHTTPDoError(req, err)
133 }
134
135 defer func() { _ = resp.Body.Close() }()
136
137 if resp.StatusCode/100 != 2 {
138 return parseError(req, resp)
139 }
140
141 if result == nil {
142 return nil
143 }
144
145 raw, err := io.ReadAll(resp.Body)
146 if err != nil {
147 return errutils.NewReadResponseError(req, resp.StatusCode, err)
148 }
149
150 err = json.Unmarshal(raw, result)
151 if err != nil {
152 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
153 }
154
155 return nil
156 }
157
158 func newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
159 var body io.Reader = http.NoBody
160
161 if payload != nil {
162 buf := new(bytes.Buffer)
163
164 err := json.NewEncoder(buf).Encode(payload)
165 if err != nil {
166 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
167 }
168
169 values := url.Values{}
170 values.Set("config", buf.String())
171 body = strings.NewReader(values.Encode())
172 }
173
174 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
175 if err != nil {
176 return nil, fmt.Errorf("unable to create request: %w", err)
177 }
178
179 req.Header.Set("Accept", "application/json")
180
181 if payload != nil {
182 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
183 }
184
185 return req, nil
186 }
187
188 func parseError(req *http.Request, resp *http.Response) error {
189 raw, _ := io.ReadAll(resp.Body)
190
191 var errAPI APIError
192
193 err := json.Unmarshal(raw, &errAPI)
194 if err != nil {
195 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
196 }
197
198 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, &errAPI)
199 }
200