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 )
16
17 // DefaultBaseURL the default API endpoint.
18 const defaultBaseURL = "https://manager.shellrent.com/api2"
19
20 const authorizationHeader = "Authorization"
21
22 // Client the Shellrent API client.
23 type Client struct {
24 username string
25 token string
26
27 baseURL *url.URL
28 HTTPClient *http.Client
29 }
30
31 // NewClient Creates a new Client.
32 func NewClient(username, token string) *Client {
33 baseURL, _ := url.Parse(defaultBaseURL)
34
35 return &Client{
36 token: token,
37 username: username,
38 baseURL: baseURL,
39 HTTPClient: &http.Client{Timeout: 10 * time.Second},
40 }
41 }
42
43 // ListServices lists service IDs.
44 // https://api.shellrent.com/elenco-dei-servizi-acquistati
45 func (c *Client) ListServices(ctx context.Context) ([]int, error) {
46 endpoint := c.baseURL.JoinPath("purchase")
47
48 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
49 if err != nil {
50 return nil, err
51 }
52
53 result := Response[[]IntOrString]{}
54
55 err = c.do(req, &result)
56 if err != nil {
57 return nil, err
58 }
59
60 if result.Code != 0 {
61 return nil, result.Base
62 }
63
64 var ids []int
65
66 for _, datum := range result.Data {
67 ids = append(ids, datum.Value())
68 }
69
70 return ids, nil
71 }
72
73 // GetServiceDetails gets service details.
74 // https://api.shellrent.com/dettagli-servizio-acquistato
75 func (c *Client) GetServiceDetails(ctx context.Context, serviceID int) (*ServiceDetails, error) {
76 endpoint := c.baseURL.JoinPath("purchase", "details", strconv.Itoa(serviceID))
77
78 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
79 if err != nil {
80 return nil, err
81 }
82
83 result := Response[*ServiceDetails]{}
84
85 err = c.do(req, &result)
86 if err != nil {
87 return nil, err
88 }
89
90 if result.Code != 0 {
91 return nil, result.Base
92 }
93
94 return result.Data, nil
95 }
96
97 // GetDomainDetails gets domain details.
98 // https://api.shellrent.com/dettagli-dominio
99 func (c *Client) GetDomainDetails(ctx context.Context, domainID int) (*DomainDetails, error) {
100 endpoint := c.baseURL.JoinPath("domain", "details", strconv.Itoa(domainID))
101
102 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
103 if err != nil {
104 return nil, err
105 }
106
107 result := Response[*DomainDetails]{}
108
109 err = c.do(req, &result)
110 if err != nil {
111 return nil, err
112 }
113
114 if result.Code != 0 {
115 return nil, result.Base
116 }
117
118 return result.Data, nil
119 }
120
121 // CreateRecord created a record.
122 // https://api.shellrent.com/creazione-record-dns-di-un-dominio
123 func (c *Client) CreateRecord(ctx context.Context, domainID int, record Record) (int, error) {
124 endpoint := c.baseURL.JoinPath("dns_record", "store", strconv.Itoa(domainID))
125
126 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
127 if err != nil {
128 return 0, err
129 }
130
131 result := Response[*Record]{}
132
133 err = c.do(req, &result)
134 if err != nil {
135 return 0, err
136 }
137
138 if result.Code != 0 {
139 return 0, result.Base
140 }
141
142 return result.Data.ID.Value(), nil
143 }
144
145 // DeleteRecord deletes a record.
146 // https://api.shellrent.com/eliminazione-record-dns-di-un-dominio
147 func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error {
148 endpoint := c.baseURL.JoinPath("dns_record", "remove", strconv.Itoa(domainID), strconv.Itoa(recordID))
149
150 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
151 if err != nil {
152 return err
153 }
154
155 result := Response[any]{}
156
157 err = c.do(req, &result)
158 if err != nil {
159 return err
160 }
161
162 if result.Code != 0 {
163 return result.Base
164 }
165
166 return nil
167 }
168
169 func (c *Client) do(req *http.Request, result any) error {
170 req.Header.Set(authorizationHeader, c.username+"."+c.token)
171
172 resp, err := c.HTTPClient.Do(req)
173 if err != nil {
174 return errutils.NewHTTPDoError(req, err)
175 }
176
177 defer func() { _ = resp.Body.Close() }()
178
179 if resp.StatusCode/100 != 2 {
180 return parseError(req, resp)
181 }
182
183 raw, err := io.ReadAll(resp.Body)
184 if err != nil {
185 return errutils.NewReadResponseError(req, resp.StatusCode, err)
186 }
187
188 err = json.Unmarshal(raw, result)
189 if err != nil {
190 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
191 }
192
193 return nil
194 }
195
196 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
197 buf := new(bytes.Buffer)
198
199 if payload != nil {
200 err := json.NewEncoder(buf).Encode(payload)
201 if err != nil {
202 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
203 }
204 }
205
206 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
207 if err != nil {
208 return nil, fmt.Errorf("unable to create request: %w", err)
209 }
210
211 req.Header.Set("Accept", "application/json")
212
213 if payload != nil {
214 req.Header.Set("Content-Type", "application/json")
215 }
216
217 return req, nil
218 }
219
220 func parseError(req *http.Request, resp *http.Response) error {
221 raw, _ := io.ReadAll(resp.Body)
222
223 var response Base
224
225 err := json.Unmarshal(raw, &response)
226 if err != nil {
227 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
228 }
229
230 return response
231 }
232
233 // TTLRounder rounds the given TTL in seconds to the next accepted value.
234 // Accepted TTL values are:
235 // - 3600
236 // - 14400
237 // - 28800
238 // - 57600
239 // - 86400
240 func TTLRounder(ttl int) int {
241 for _, validTTL := range []int{3600, 14400, 28800, 57600, 86400} {
242 if ttl <= validTTL {
243 return validTTL
244 }
245 }
246
247 return 3600
248 }
249