-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnbiclient.go
283 lines (248 loc) · 9.87 KB
/
nbiclient.go
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
package xmcnbiclient
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const (
// AccessSchemeHTTP is used to define that HTTP shall be used for communication with XMC.
AccessSchemeHTTP string = "http"
// AccessSchemeHTTPS is used to define that HTTPS shall be used for communication with XMC.
AccessSchemeHTTPS string = "https"
// AuthTypeBasic is used to define that basic auth shall be used for authentication with XMC.
AuthTypeBasic string = "basic"
// AuthTypeOAuth is used to define that OAuth shall be used for authentication with XMC.
AuthTypeOAuth string = "oauth"
)
// Authentication stores credentials to use when authenticating with XMC.
type Authentication struct {
// Type must be set to either AuthTypeBasic or AuthTypeOAuth.
Type string
// UserID stores the username (basic auth) or client ID (OAuth).
UserID string
// Secret stores the password (basic auth) or client secret (OAuth).
Secret string
}
// NBIClient encapsulates the actual HTTP client that communicates with XMC.
// Use New() to obtain an usable instance. All fields should be treated as read-only; functions are provided where changes shall be possible.
type NBIClient struct {
// httpClient is the actual HTTP client. Should not be manipulated directly.
httpClient http.Client
// UserAgent is transmitted as the User-Agent header with each request.
UserAgent string
// AccessScheme is used to define whether HTTP or HTTPS shall be used for communication with XMC.
AccessScheme string
// HTTPHost is the IP or hostname of the XMC server. Should not be manipulated directly.
HTTPHost string
// HTTPPort is the TCP port where XMC is listening.
HTTPPort uint
// BasePath is the path where XMC is available on HTTPHost.
BasePath string
// Authentication stores authentication information.
Authentication Authentication
// AccessToken is used to store the OAuth token when it is used.
AccessToken OAuthToken
}
// New is used to create an usable instance of NBIClient.
// By default a new instance will use HTTPS to port 8443 with strict certificate checking. The HTTP timeout is set to 5 seconds. Authentication must be set manually before trying to send a query to XMC.
func New(host string) NBIClient {
var c NBIClient
c.httpClient = http.Client{}
c.SetUserAgent(fmt.Sprintf("%s/%s", moduleName, moduleVersion))
c.UseHTTPS()
c.HTTPHost = host
c.SetPort(8443)
c.SetBasePath("")
c.UseSecureHTTPS()
c.SetTimeout(5)
return c
}
// String returns a compact representation of the authentication method and values with a masked secret.
func (a Authentication) String() string {
return fmt.Sprintf("%s{%s:%s}", a.Type, a.UserID, "***")
}
// StringWithSecret returns a compact representation of the authentication method and values with the plain text secret.
func (a Authentication) StringWithSecret() string {
return fmt.Sprintf("%s{%s:%s}", a.Type, a.UserID, a.Secret)
}
// String returns a usable string reprensentation of a NBIClient instance.
func (c NBIClient) String() string {
return fmt.Sprintf("%s://%s@%s:%d/", c.AccessScheme, c.Authentication, c.HTTPHost, c.HTTPPort)
}
// SetUserAgent sets the User-Agent HTTP header.
func (c *NBIClient) SetUserAgent(ua string) {
c.UserAgent = ua
}
// UseHTTP sets the protocol to HTTP for the NBIClient instance.
func (c *NBIClient) UseHTTP() {
c.AccessScheme = AccessSchemeHTTP
}
// UseHTTPS sets the protocol to HTTPS for the NBIClient instance.
func (c *NBIClient) UseHTTPS() {
c.AccessScheme = AccessSchemeHTTPS
}
// SetPort sets the TCP port where XMC is listening for the NBIClient instance.
func (c *NBIClient) SetPort(port uint) error {
if httpMinPort <= port && httpMaxPort >= port {
c.HTTPPort = port
return nil
}
return fmt.Errorf("port out of range (1 - 65535)")
}
// SetBasePath sets the path where XMC is available on the host.
func (c *NBIClient) SetBasePath(path string) {
c.BasePath = path
}
// SetTimeout sets the HTTP timeout in seconds for the NBIClient instance.
func (c *NBIClient) SetTimeout(seconds uint) error {
if httpMinTimeout <= seconds && httpMaxTimeout >= seconds {
c.httpClient.Timeout = time.Second * time.Duration(seconds)
return nil
}
return fmt.Errorf("timeout out of range (1 - 300)")
}
// UseSecureHTTPS enforces strict HTTPS certificate checking.
func (c *NBIClient) UseSecureHTTPS() {
httpTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
c.httpClient.Transport = httpTransport
}
// UseInsecureHTTPS disables strict HTTPS certificate checking.
func (c *NBIClient) UseInsecureHTTPS() {
httpTransport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
c.httpClient.Transport = httpTransport
}
// UseBasicAuth informs the NBIClient instance to use HTTP Basic Auth along with the provided credentials.
func (c *NBIClient) UseBasicAuth(username string, password string) {
c.Authentication = Authentication{Type: AuthTypeBasic, UserID: username, Secret: password}
}
// UseOAuth informs the NBIClient instance to use OAuth along with the provided credentials.
func (c *NBIClient) UseOAuth(clientid string, secret string) {
c.Authentication = Authentication{Type: AuthTypeOAuth, UserID: clientid, Secret: secret}
}
// BaseURL returns the base URL the instance of NBIClient uses to contact XMC.
func (c *NBIClient) BaseURL() string {
return fmt.Sprintf("%s://%s:%d%s", c.AccessScheme, c.HTTPHost, c.HTTPPort, c.BasePath)
}
// TokenURL returns the URL the instance of NBIClient uses for obtaining an OAuth token.
func (c *NBIClient) TokenURL() string {
return fmt.Sprintf("%s/oauth/token/access-token?grant_type=client_credentials", c.BaseURL())
}
// APIURL returns the URL the instance of NBIClient sends queries to.
func (c *NBIClient) APIURL() string {
return fmt.Sprintf("%s/nbi/graphql", c.BaseURL())
}
// RetrieveOAuthToken tries to obtain a valid OAuth token from XMC and to decode it.
func (c *NBIClient) RetrieveOAuthToken() error {
// Empty token structure to start with.
var tokenData OAuthToken
// Only continue if OAuth is actually configured.
if c.Authentication.Type != AuthTypeOAuth {
return fmt.Errorf("auth type not set to OAuth")
}
// Generate an actual HTTP request.
req, reqErr := http.NewRequest(http.MethodPost, c.TokenURL(), nil)
if reqErr != nil {
return fmt.Errorf("could not create HTTP(S) request: %s", reqErr)
}
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Accept", jsonMimeType)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(c.Authentication.UserID, c.Authentication.Secret)
// Try to get a result from the API.
res, resErr := c.httpClient.Do(req)
if resErr != nil {
return fmt.Errorf("could not connect to XMC: %s", resErr)
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("got status code %d instead of %d", res.StatusCode, http.StatusOK)
}
defer res.Body.Close()
// Check if the HTTP response has yielded the expected content type.
resContentType := res.Header.Get("Content-Type")
if strings.Index(resContentType, jsonMimeType) != 0 {
return fmt.Errorf("Content-Type %s returned instead of %s", resContentType, jsonMimeType)
}
// Read and parse the body of the HTTP response.
xmcToken, readErr := io.ReadAll(res.Body)
if readErr != nil {
return fmt.Errorf("could not read server response: %s", readErr)
}
jsonErr := json.Unmarshal(xmcToken, &tokenData)
if jsonErr != nil {
return fmt.Errorf("could not read server response: %s", jsonErr)
}
// Decode token data.
decodeErr := tokenData.Decode()
if decodeErr != nil {
return fmt.Errorf("could not decode token: %s", decodeErr)
}
// Store the complete token in the client.
c.AccessToken = tokenData
return nil
}
// QueryAPI sends a request to the XMC API and returns the JSON result as a byte array.
func (c *NBIClient) QueryAPI(query string) ([]byte, error) {
var rval []byte
// Only continue if an authentication method has been defined.
if c.Authentication.Type == "" {
return rval, fmt.Errorf("no authentication method defined")
}
// Wrap the query into a JSON object.
jsonQuery, jsonQueryErr := json.Marshal(map[string]string{"query": query})
if jsonQueryErr != nil {
return rval, fmt.Errorf("could not encode query into JSON: %s", jsonQueryErr)
}
// Create an HTTP request.
req, reqErr := http.NewRequest(http.MethodPost, c.APIURL(), bytes.NewBuffer(jsonQuery))
if reqErr != nil {
return rval, fmt.Errorf("could not create HTTP(S) request: %s", reqErr)
}
// Set some basic request headers.
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Content-Type", jsonMimeType)
req.Header.Set("Accept", jsonMimeType)
// Set the authentication header based on the chosen authentication method.
if c.Authentication.Type == AuthTypeOAuth {
// If the stored OAuth token is invalid, try to renew it.
if c.AccessToken.IsValid() != true {
tokenErr := c.RetrieveOAuthToken()
if tokenErr != nil {
return rval, fmt.Errorf("could not retrieve fresh OAuth token: %s", tokenErr)
}
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AccessToken.RawToken))
} else {
req.SetBasicAuth(c.Authentication.UserID, c.Authentication.Secret)
}
// Try to get a result from the API.
res, resErr := c.httpClient.Do(req)
if resErr != nil {
return rval, fmt.Errorf("Could not connect to XMC: %s", resErr)
}
if res.StatusCode != 200 {
return rval, fmt.Errorf("Got status code %d instead of 200", res.StatusCode)
}
defer res.Body.Close()
// Check if the HTTP response has yielded the expected content type.
resContentType := res.Header.Get("Content-Type")
if strings.Index(resContentType, jsonMimeType) != 0 {
return rval, fmt.Errorf("Content-Type %s returned instead of %s", resContentType, jsonMimeType)
}
// Read the body of the HTTP response.
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
return rval, fmt.Errorf("Could not read server response: %s", readErr)
}
return body, nil
}