forked from uyuni-project/uyuni-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.go
More file actions
366 lines (315 loc) · 9.64 KB
/
Copy pathapi.go
File metadata and controls
366 lines (315 loc) · 9.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0
package api
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)
// AddAPIFlags is a helper to include api details for the provided command tree.
func AddAPIFlags(cmd *cobra.Command) {
cmd.PersistentFlags().String("api-server", "", L("FQDN of the server to connect to"))
cmd.PersistentFlags().String("api-user", "", L("API user username"))
cmd.PersistentFlags().String("api-password", "", L("Password for the API user"))
cmd.PersistentFlags().String("api-cacert", "", L("Path to a cert file of the CA"))
cmd.PersistentFlags().Bool("api-insecure", false, L("If set, server certificate will not be checked for validity"))
}
var redactRegex = regexp.MustCompile(`(((pxt-session-cookie)|(JSESSIONID))=)[^ ";]+`)
func redactHeaders(header string) string {
return redactRegex.ReplaceAllString(header, "${1}<REDACTED>")
}
func logTraceHeader(v *http.Header) {
// Return early when not in trace loglevel
if log.Logger.GetLevel() != zerolog.TraceLevel {
return
}
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return
}
log.Trace().Msg(redactHeaders(string(b)))
}
func (c *APIClient) sendRequest(req *http.Request) (*http.Response, error) {
log.Debug().Msgf("Sending %s request %s", req.Method, req.URL)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json; charset=utf-8")
if c.AuthCookie != nil {
req.AddCookie(c.AuthCookie)
}
logTraceHeader(&req.Header)
res, err := c.Client.Do(req)
if err != nil {
log.Trace().Err(err).Msgf("Request failed")
return nil, err
}
logTraceHeader(&res.Header)
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
if res.StatusCode == 401 {
return nil, errors.New(L("401: unauthorized"))
}
var errResponse map[string]string
if res.Body != nil {
body, err := io.ReadAll(res.Body)
if err == nil {
if err = json.Unmarshal(body, &errResponse); err == nil {
errorMessage := fmt.Sprintf("%d: '%s'", res.StatusCode, errResponse["message"])
return nil, errors.New(errorMessage)
}
errorMessage := fmt.Sprintf("%d: '%s'", res.StatusCode, string(body))
return nil, errors.New(errorMessage)
}
}
return nil, fmt.Errorf(L("unknown error: %d"), res.StatusCode)
}
log.Debug().Msgf("Received response with code %d", res.StatusCode)
return res, nil
}
// Init returns a HTTPClient object for further API use.
//
// Provided connectionDetails must have Server specified with FQDN to the
// target host.
//
// Optionaly connectionDetails can have user name and password set and Init
// will try to login to the host.
// caCert can be set to use custom CA certificate to validate target host.
func Init(conn *ConnectionDetails) (*APIClient, error) {
// Load stored credentials as it also loads up server URL and CApath
getStoredConnectionDetails(conn)
caCertPool, err := x509.SystemCertPool()
if err != nil {
log.Warn().Msg(err.Error())
}
if conn.CApath != "" {
caCert, err := os.ReadFile(conn.CApath)
if err != nil {
log.Fatal().Msg(err.Error())
}
caCertPool.AppendCertsFromPEM(caCert)
}
if conn.Server == "" {
return nil, errors.New(L("server URL is not provided"))
}
client := &APIClient{
Details: conn,
BaseURL: fmt.Sprintf("https://%s%s", conn.Server, rootPathApiv1),
Client: &http.Client{
Timeout: time.Minute,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
InsecureSkipVerify: conn.Insecure,
},
},
},
}
if conn.Cookie != "" {
client.AuthCookie = &http.Cookie{
Name: "pxt-session-cookie",
Value: conn.Cookie,
}
}
return client, err
}
// Login to the server using stored or provided credentials.
func (c *APIClient) Login() error {
if c.Details.InSession {
if err := c.sessionValidity(); err == nil {
// Session is valid
return nil
}
log.Warn().Msg(L("Cached session is expired."))
if err := RemoveLoginCreds(); err != nil {
log.Warn().Err(err).Msg(L("Failed to remove stored credentials!"))
}
}
if err := getLoginCredentials(c.Details); err != nil {
return err
}
return c.login()
}
func (c *APIClient) login() error {
conn := c.Details
url := fmt.Sprintf("%s/%s", c.BaseURL, "auth/login")
data := map[string]string{
"login": conn.User,
"password": conn.Password,
}
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msg(L("Unable to create login data"))
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
res, err := c.sendRequest(req)
if err != nil {
return err
}
var response map[string]interface{}
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
return err
}
if !response["success"].(bool) {
return fmt.Errorf(response["message"].(string))
}
cookies := res.Cookies()
for _, cookie := range cookies {
if cookie.Name == "pxt-session-cookie" && cookie.MaxAge > 0 {
c.AuthCookie = cookie
break
}
}
if c.AuthCookie == nil {
return errors.New(L("auth cookie not found in login response"))
}
return nil
}
func (c *APIClient) sessionValidity() error {
// This is how spacecmd does it
_, err := c.Get("user/listAssignableRoles")
return err
}
// Logout from the server and remove localy stored session key.
func (c *APIClient) Logout() error {
if _, err := c.Post("auth/logout", nil); err != nil {
return utils.Errorf(err, L("failed to logout from the server"))
}
return RemoveLoginCreds()
}
// ValidateCreds checks if the login credentials are valid.
func (c *APIClient) ValidateCreds() bool {
err := c.Login()
return err == nil
}
// Post issues a POST HTTP request to the API target, without validating the endpoint.
//
// `path` specifies an API endpoint
// `data` contains a map of values to add to the POST query. `data` are serialized to the JSON
//
// returns a raw HTTP Response.
func (c *APIClient) Post(path string, data map[string]interface{}) (*http.Response, error) {
url := fmt.Sprintf("%s/%s", c.BaseURL, path)
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msg(L("Unable to convert data to JSON"))
return nil, err
}
log.Trace().Msgf("payload: %s", string(jsonData))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
res, err := c.sendRequest(req)
if err != nil {
return nil, err
}
return res, nil
}
// PostChecked validates the API endpoint before issuing a POST request.
func (c *APIClient) PostChecked(path string, endpoint string, data map[string]interface{}) (*http.Response, error) {
if err := c.validateEndpoint(endpoint); err != nil {
return nil, err
}
return c.Post(path, data)
}
// PostChecked issues a validated POST request to the API target using the client and decodes the response.
func PostChecked[T interface{}](
client *APIClient, path string, namespace string, data map[string]interface{},
) (*APIResponse[T], error) {
res, err := client.PostChecked(path, namespace, data)
if err != nil {
return nil, err
}
return decodeAPIResponse[T](res)
}
// Get issues GET HTTP request to the API target
//
// `path` specifies API endpoint together with query options
//
// returns a raw HTTP Response.
func (c *APIClient) Get(path string) (*http.Response, error) {
url := fmt.Sprintf("%s/%s", c.BaseURL, path)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := c.sendRequest(req)
if err != nil {
return nil, err
}
return res, nil
}
// Post issues a POST HTTP request to the API target using the client and decodes the response.
//
// `path` specifies an API endpoint
// `data` contains a map of values to add to the POST query. `data` are serialized to the JSON
//
// returns a deserialized JSON data to the map.
func Post[T interface{}](client *APIClient, path string, data map[string]interface{}) (*APIResponse[T], error) {
res, err := client.Post(path, data)
if err != nil {
return nil, err
}
defer res.Body.Close()
var response APIResponse[T]
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
log.Trace().Msgf("response: %s", string(body))
if err = json.Unmarshal(body, &response); err != nil {
return nil, err
}
return &response, nil
}
// Get issues an HTTP GET request to the API using the client and decodes the response.
//
// `path` specifies API endpoint together with query options
//
// returns an ApiResponse with the decoded result.
func Get[T interface{}](client *APIClient, path string) (*APIResponse[T], error) {
res, err := client.Get(path)
if err != nil {
return nil, err
}
return decodeAPIResponse[T](res)
}
// GetChecked validates the API namespace before issuing a GET request.
func (c *APIClient) GetChecked(path string, namespace string) (*http.Response, error) {
if err := c.validateEndpoint(namespace); err != nil {
return nil, err
}
return c.Get(path)
}
// GetChecked issues a validated GET request to the API using the client and decodes the response.
func GetChecked[T interface{}](client *APIClient, path string, endpoint string) (*APIResponse[T], error) {
res, err := client.GetChecked(path, endpoint)
if err != nil {
return nil, err
}
return decodeAPIResponse[T](res)
}
func decodeAPIResponse[T interface{}](res *http.Response) (*APIResponse[T], error) {
defer res.Body.Close()
var response APIResponse[T]
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, err
}
return &response, nil
}