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 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 "golang.org/x/oauth2"
16 )
17
18 const defaultBaseURL = "https://api.ns1.hosttech.eu/api"
19
20 // Client a Hosttech 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 if hc == nil {
31 hc = &http.Client{Timeout: 10 * time.Second}
32 }
33
34 return &Client{baseURL: baseURL, httpClient: hc}
35 }
36
37 // GetZones Get a list of all zones.
38 // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones
39 func (c *Client) GetZones(ctx context.Context, query string, limit, offset int) ([]Zone, error) {
40 endpoint := c.baseURL.JoinPath("user", "v1", "zones")
41
42 values := endpoint.Query()
43 values.Set("query", query)
44
45 if limit > 0 {
46 values.Set("limit", strconv.Itoa(limit))
47 }
48
49 if offset > 0 {
50 values.Set("offset", strconv.Itoa(offset))
51 }
52
53 endpoint.RawQuery = values.Encode()
54
55 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
56 if err != nil {
57 return nil, fmt.Errorf("create request: %w", err)
58 }
59
60 result := apiResponse[[]Zone]{}
61
62 err = c.do(req, &result)
63 if err != nil {
64 return nil, err
65 }
66
67 return result.Data, nil
68 }
69
70 // GetZone Get a single zone.
71 // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones__zoneId_
72 func (c *Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) {
73 endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID)
74
75 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
76 if err != nil {
77 return nil, fmt.Errorf("create request: %w", err)
78 }
79
80 result := apiResponse[*Zone]{}
81
82 err = c.do(req, &result)
83 if err != nil {
84 return nil, err
85 }
86
87 return result.Data, nil
88 }
89
90 // GetRecords Returns a list of all records for the given zone.
91 // https://api.ns1.hosttech.eu/api/documentation/#/Records/get_api_user_v1_zones__zoneId__records
92 func (c *Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]Record, error) {
93 endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records")
94
95 values := endpoint.Query()
96
97 if recordType != "" {
98 values.Set("type", recordType)
99 }
100
101 endpoint.RawQuery = values.Encode()
102
103 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
104 if err != nil {
105 return nil, fmt.Errorf("create request: %w", err)
106 }
107
108 result := apiResponse[[]Record]{}
109
110 err = c.do(req, &result)
111 if err != nil {
112 return nil, err
113 }
114
115 return result.Data, nil
116 }
117
118 // AddRecord Adds a new record to the zone and returns the newly created record.
119 // https://api.ns1.hosttech.eu/api/documentation/#/Records/post_api_user_v1_zones__zoneId__records
120 func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {
121 endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records")
122
123 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
124 if err != nil {
125 return nil, fmt.Errorf("create request: %w", err)
126 }
127
128 result := apiResponse[*Record]{}
129
130 err = c.do(req, &result)
131 if err != nil {
132 return nil, err
133 }
134
135 return result.Data, nil
136 }
137
138 // DeleteRecord Deletes a single record for the given id.
139 // https://api.ns1.hosttech.eu/api/documentation/#/Records/delete_api_user_v1_zones__zoneId__records__recordId_
140 func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {
141 endpoint := c.baseURL.JoinPath("user", "v1", "zones", zoneID, "records", recordID)
142
143 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
144 if err != nil {
145 return fmt.Errorf("create request: %w", err)
146 }
147
148 return c.do(req, nil)
149 }
150
151 func (c *Client) do(req *http.Request, result any) error {
152 resp, errD := c.httpClient.Do(req)
153 if errD != nil {
154 return errutils.NewHTTPDoError(req, errD)
155 }
156
157 defer func() { _ = resp.Body.Close() }()
158
159 switch resp.StatusCode {
160 case http.StatusOK, http.StatusCreated:
161 raw, err := io.ReadAll(resp.Body)
162 if err != nil {
163 return errutils.NewReadResponseError(req, resp.StatusCode, err)
164 }
165
166 err = json.Unmarshal(raw, result)
167 if err != nil {
168 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
169 }
170
171 return nil
172
173 case http.StatusNoContent:
174 return nil
175
176 default:
177 return parseError(req, resp)
178 }
179 }
180
181 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
182 buf := new(bytes.Buffer)
183
184 if payload != nil {
185 err := json.NewEncoder(buf).Encode(payload)
186 if err != nil {
187 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
188 }
189 }
190
191 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
192 if err != nil {
193 return nil, fmt.Errorf("unable to create request: %w", err)
194 }
195
196 req.Header.Set("Accept", "application/json")
197
198 if payload != nil {
199 req.Header.Set("Content-Type", "application/json")
200 }
201
202 return req, nil
203 }
204
205 func parseError(req *http.Request, resp *http.Response) error {
206 raw, _ := io.ReadAll(resp.Body)
207
208 errAPI := &APIError{StatusCode: resp.StatusCode}
209
210 err := json.Unmarshal(raw, errAPI)
211 if err != nil {
212 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
213 }
214
215 return errAPI
216 }
217
218 func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
219 if client == nil {
220 client = &http.Client{Timeout: 5 * time.Second}
221 }
222
223 client.Transport = &oauth2.Transport{
224 Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
225 Base: client.Transport,
226 }
227
228 return client
229 }
230