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/http/cookiejar"
12 "net/url"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
17 "golang.org/x/net/publicsuffix"
18 )
19
20 // Client the Gravity API client.
21 type Client struct {
22 username string
23 password string
24
25 baseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(serverURL, username, password string) (*Client, error) {
31 if username == "" || password == "" {
32 return nil, errors.New("credentials missing")
33 }
34
35 if serverURL == "" {
36 return nil, errors.New("server URL missing")
37 }
38
39 baseURL, err := url.Parse(serverURL)
40 if err != nil {
41 return nil, err
42 }
43
44 return &Client{
45 username: username,
46 password: password,
47 baseURL: baseURL,
48 HTTPClient: &http.Client{Timeout: 10 * time.Second},
49 }, nil
50 }
51
52 func (c *Client) Login(ctx context.Context) (*Auth, error) {
53 jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
54 if err != nil {
55 return nil, err
56 }
57
58 c.HTTPClient.Jar = jar
59
60 login := Login{
61 Username: c.username,
62 Password: c.password,
63 }
64
65 endpoint := c.baseURL.JoinPath("api", "v1", "auth", "login")
66
67 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, login)
68 if err != nil {
69 return nil, err
70 }
71
72 result := &Auth{}
73
74 err = c.do(req, result)
75 if err != nil {
76 return nil, err
77 }
78
79 return result, nil
80 }
81
82 func (c *Client) Me(ctx context.Context) (*UserInfo, error) {
83 endpoint := c.baseURL.JoinPath("api", "v1", "auth", "me")
84
85 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
86 if err != nil {
87 return nil, err
88 }
89
90 result := &UserInfo{}
91
92 err = c.do(req, result)
93 if err != nil {
94 return nil, err
95 }
96
97 return result, err
98 }
99
100 func (c *Client) GetDNSZones(ctx context.Context, name string) ([]Zone, error) {
101 endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones")
102
103 if name != "" {
104 query := endpoint.Query()
105 query.Set("name", name)
106 endpoint.RawQuery = query.Encode()
107 }
108
109 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
110 if err != nil {
111 return nil, err
112 }
113
114 result := Zones{}
115
116 err = c.do(req, &result)
117 if err != nil {
118 return nil, err
119 }
120
121 return result.Zones, nil
122 }
123
124 func (c *Client) CreateDNSRecord(ctx context.Context, zone string, record Record) error {
125 endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records")
126
127 query := endpoint.Query()
128
129 query.Set("zone", zone)
130 query.Set("hostname", record.Hostname)
131
132 // When the UID is the same as an existing one, the record is updated, else a new record is created.
133 // An explicit UID is not required to create a record.
134 if record.UID != "" {
135 query.Set("uid", record.UID)
136 }
137
138 endpoint.RawQuery = query.Encode()
139
140 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
141 if err != nil {
142 return err
143 }
144
145 return c.do(req, nil)
146 }
147
148 func (c *Client) DeleteDNSRecord(ctx context.Context, zone string, record Record) error {
149 endpoint := c.baseURL.JoinPath("api", "v1", "dns", "zones", "records")
150
151 query := endpoint.Query()
152
153 query.Set("zone", zone)
154 query.Set("hostname", record.Hostname)
155 query.Set("uid", record.UID)
156 query.Set("type", record.Type)
157
158 endpoint.RawQuery = query.Encode()
159
160 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
161 if err != nil {
162 return err
163 }
164
165 return c.do(req, nil)
166 }
167
168 func (c *Client) do(req *http.Request, result any) error {
169 useragent.SetHeader(req.Header)
170
171 resp, err := c.HTTPClient.Do(req)
172 if err != nil {
173 return errutils.NewHTTPDoError(req, err)
174 }
175
176 defer func() { _ = resp.Body.Close() }()
177
178 if resp.StatusCode/100 != 2 {
179 return parseError(req, resp)
180 }
181
182 if result == nil {
183 return nil
184 }
185
186 raw, err := io.ReadAll(resp.Body)
187 if err != nil {
188 return errutils.NewReadResponseError(req, resp.StatusCode, err)
189 }
190
191 err = json.Unmarshal(raw, result)
192 if err != nil {
193 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
194 }
195
196 return nil
197 }
198
199 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
200 buf := new(bytes.Buffer)
201
202 if payload != nil {
203 err := json.NewEncoder(buf).Encode(payload)
204 if err != nil {
205 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
206 }
207 }
208
209 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
210 if err != nil {
211 return nil, fmt.Errorf("unable to create request: %w", err)
212 }
213
214 req.Header.Set("Accept", "application/json")
215
216 if payload != nil {
217 req.Header.Set("Content-Type", "application/json")
218 }
219
220 return req, nil
221 }
222
223 func parseError(req *http.Request, resp *http.Response) error {
224 raw, _ := io.ReadAll(resp.Body)
225
226 var errAPI APIError
227
228 err := json.Unmarshal(raw, &errAPI)
229 if err != nil {
230 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
231 }
232
233 return &errAPI
234 }
235