-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi.go
426 lines (369 loc) · 13.8 KB
/
api.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
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
package main
import (
"bytes"
"encoding/json"
"fmt"
"golang.org/x/term"
"io"
"io/ioutil"
"net/http"
"os/user"
"strings"
"syscall"
"time"
. "github.com/dirtman/sitepkg"
)
// Supported API authentication methods:
const AuthMethodBasic = "basic"
const AuthMethodBearer = "bearer"
// For convenience, hold the basic API parameter:
type APIConfig struct {
BaseURL string
AuthMethod string
AuthToken string
}
var API APIConfig
// Fetch exit status; distinguish "not found" from other errors.
type FetchStatus int
const FetchStatusOK FetchStatus = 0
const FetchStatusError FetchStatus = 1
const FetchStatusNotFound FetchStatus = 2
/*****************************************************************************\
API GET, POST, PUT and DELETE Requests
\*****************************************************************************/
func APIGet(url string, headers ...http.Header) (body []byte, err error) {
return APIRequest("GET", url, nil, headers...)
}
func APIPost(url string, data interface{}, headers ...http.Header) (body []byte, err error) {
return APIRequest("POST", url, data, headers...)
}
func APIPut(url string, data interface{}, headers ...http.Header) (body []byte, err error) {
return APIRequest("PUT", url, data, headers...)
}
func APIDelete(url string, data interface{}, headers ...http.Header) (body []byte, err error) {
return APIRequest("DELETE", url, data, headers...)
}
func APIPatch(url string, data interface{}, headers ...http.Header) (body []byte, err error) {
return APIRequest("PATCH", url, data, headers...)
}
func APIRequest(method string, url string, data interface{}, headers ...http.Header) ([]byte, error) {
var body, dataJson []byte
var payload io.Reader
var err error
// Set the timeout for the request:
seconds, err := GetIntOpt("HTTPTimeout")
if err != nil {
if !strings.Contains(err.Error(), ConfErrNoSuchOption) {
return body, Error("failure getting current setting of \"HTTPTimeout\": %s", err)
} else {
seconds = 10
}
}
timeout := time.Duration(time.Duration(seconds) * time.Second)
urlPath := API.BaseURL + url
if Debug {
Show("Method: \"%s\".", method)
Show("URL: \"%s\".", urlPath)
}
if data != nil {
switch t := data.(type) {
default:
return body, Error("unknown body type provided (%T)", t)
case string:
payload = bytes.NewBuffer([]byte(data.(string)))
if Debug {
Show("Data (string): \"%v\".", data)
Show("Payload: \"%s\".", data.(string))
}
case interface{}:
if data != nil {
if dataJson, err = json.Marshal(data); err != nil {
return body, err
}
payload = bytes.NewBuffer(dataJson)
}
if Debug {
Show("Data (any): \"%v\".", data)
Show("Payload: \"%s\".", dataJson)
}
}
}
// Create a new http.Request.
req, err := http.NewRequest(method, urlPath, payload)
if err != nil {
return body, Error("error getting url \"%s\": %v", urlPath, err)
}
// Configure auth for the new request.
err = SetHTTPAuth(req)
if err != nil {
return body, Error("failure setting up API %s authentication: %s", API.AuthMethod, err)
}
// Set the headers for the request. If no headers are provided, we'll assume a
// JSON Content-Type is desired.
if len(headers) == 0 {
req.Header.Set("Content-Type", "application/json")
} else {
for _, httpHeader := range headers {
for header, values := range httpHeader {
for _, value := range values {
req.Header.Set(header, value)
}
}
}
}
// Make the specified request
client := &http.Client{Timeout: timeout}
resp, err := client.Do(req)
if err != nil {
return body, Error("HTTP request failed: %v", err)
}
defer resp.Body.Close()
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return body, Error("HTTP failure reading response: %v", err)
}
if Debug {
Show("HTTP Response Status: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
if body != nil {
// Show("%s:\n%s", "Response body", body)
Show("Response body:")
ShowBody(body)
}
}
// For convenience to the caller, convert an empty body to nil:
if len(body) == 0 {
body = nil
}
// We'll treat a 2xx as success and return a nil error; for all other codes,
// keep the original integer code intact, as first part of error string, to
// allow the calling program to easily decipher it as an error or not.
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
return body, nil
}
return body, Error("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
}
/*****************************************************************************\
Handle the authentication for an http.Request.
Globals: API - holds the configuration parmaters of the API.
\*****************************************************************************/
func SetHTTPAuth(req *http.Request) (err error) {
if Debug {
Show("AuthMethod: \"%v\".", API.AuthMethod)
}
// Basic Auth requires a username and a password (curl -u option). At this
// point, API.AuthToken should contain the username and password, separated
// by a ":".
if API.AuthMethod == AuthMethodBasic {
if API.AuthToken == "" {
return Error("basic auth requires a username and password")
}
authSlice := strings.Split(API.AuthToken, ":")
username := authSlice[0]
password := authSlice[1]
if username == "" {
return Error("failure getting user from APIAuthToken \"%s\"", API.AuthToken)
} else if password == "" {
return Error("failure getting password from APIAuthToken \"%s\"", API.AuthToken)
}
req.SetBasicAuth(username, password)
if Debug {
Show("Username: \"%v\".", username)
Show("Password: \"%v\".", "**************")
}
// Bearer auth requires a "Bearer: AuthToken" header. At this point,
// API.AuthToken should contain the bearer token.
} else if API.AuthMethod == AuthMethodBearer {
req.Header.Set("Authorization", "Bearer "+API.AuthToken)
if Debug {
// Don't show the whole token, but showing a bit may be helpful.
show := len(API.AuthToken)
if show > 20 {
show = 20
}
Show("Setting Auth Header: \"%s\".", "Bearer "+API.AuthToken[0:show]+"...")
}
// Unsupported AuthMethod
} else {
return Error("unknown APIAuthMethod \"%s\"", API.AuthMethod)
}
return nil
}
/*****************************************************************************\
Setup the basic API paramters.
\*****************************************************************************/
func InitAPI(args ...string) error {
var url, authType, token, tokenID string
var err error
// The caller can set an authType directly; otherwise, the caller must set
// the "APIAuthMethod" option, and we will look up the authType ourselves.
if len(args) > 1 {
return Error("InitAPI: bad call (too many args)")
} else if len(args) == 1 {
authType = args[0]
} else if authType, err = GetStringOpt("APIAuthMethod"); err != nil {
return Error("failure getting APIAuthMethod option: %v", err)
}
if authType = strings.ToLower(authType); authType == "" {
return Error("empty APIAuthMethod specified")
} else if authType != AuthMethodBearer && authType != AuthMethodBasic {
return Error("Unsupported API authentication method \"%s\".", authType)
}
if url, err = GetStringOpt("APIBaseURL"); err != nil {
return Error("failure getting APIBaseURL: %v", err)
} else if url == "" {
return Error("APIBaseURL in not configured")
} else if token, err = GetStringOpt("APIAuthToken"); err != nil {
return Error("failure getting APIAuthToken option: %v", err)
} else if tokenID, err = GetStringOpt("APIAuthTokenID"); err != nil {
return Error("failure getting APIAuthTokenID option: %v", err)
}
API.BaseURL = url
API.AuthMethod = authType
// Specifying a Username/Password takes precedence over both APIAuthToken
// and APIAuthTokenID. Note this is only applicable with Basic auth method.
if authType == AuthMethodBasic {
var username, password string
var tokenSpecified = (token != "" || tokenID != "")
if username, password, err = GetBasicAuthCreds(tokenSpecified); err != nil {
return Error("failure getting username and password: %v", err)
} else if username != "" && password != "" {
// We have a username and password, so we are done. Store the
// username and password in API.AuthToken, separated by ":".
API.AuthToken = strings.Join([]string{username, password}, ":")
return nil
}
}
// Check if an APIAuthToken has been specified; if so we are done here.
// Note that a token is allowed for both Bearer and Basic authentication.
// For Basic auth, APIAuthToken should have the form "username:password".
// Also note that APIAuthToken takes precedence over APIAuthTokenID.
if token != "" {
API.AuthToken = token
return nil
}
// We haven't found our credentials yet, so try APIAuthTokenID.
if tokenID == "" {
return Error("ran out of authentication methods to try.")
} else if token, err = GetSecret(tokenID); err != nil {
return Error("failure getting APIAuthToken from secrets file: %v", err)
} else if token == "" {
return Error("empty APIAuthToken retrieved from secrets file")
}
API.AuthToken = token
return nil
}
// GetBasicAuthCreds processes the optional Username, Password and
// PromptForPassword options.
func GetBasicAuthCreds(altMethodsAvailable bool) (string, string, error) {
var username, password string
var prompt bool
var err error
if username, err = GetStringOpt("Username"); err != nil {
return "", "", Error("failure getting username option: %v", err)
} else if password, err = GetStringOpt("Password"); err != nil {
return "", "", Error("failure getting Password option: %v", err)
} else if prompt, err = GetBoolOpt("PromptForPassword"); err != nil {
return "", "", Error("failure getting PromptForPassword option: %v", err)
}
// If a --Username option was not provided, and either: 1) no other
// auth methods have been specified, or 2) a password has been specified,
// or 3) the PromptForPassword option has been specified, then set
// username to the current user.:
if username == "" && (!altMethodsAvailable || password != "" || prompt) {
if currentUser, err := user.Current(); err != nil {
return "", "", Error("failure getting current user: %v", err)
} else if username = currentUser.Username; username == "" {
return "", "", Error("failure getting current user: got empty string")
}
}
if username != "" && password == "" {
if password, err = PromptForPassword(username); err != nil {
return "", "", Error("%v", err)
}
}
return username, password, nil
}
// PromptForPassword prompts the user for the password.
func PromptForPassword(username string) (string, error) {
var bytepw []byte
var err error
fmt.Printf("Password for %s: ", username)
if bytepw, err = term.ReadPassword(int(syscall.Stdin)); err != nil {
return "", Error("failure prompting for password: %v", err)
}
fmt.Printf("\n")
if len(bytepw) == 0 {
return "", Error("failure prompting for password: zero length password not supported")
}
return string(bytepw), nil
}
// SetAPIOptions sets the required API related options, and if used, it must be
// called before ConfigureOptions (else setting these options will have no
// effect). The caller can optionally set the required options himself instead of
// calling SetAPIOptions. Note if the caller does not provide an authMethod, the
// "APIAuthMethod" option will be set, allowing the user to choose the authMethod.
func SetAPIOptions(args ...string) error {
var authType string
authTypes := strings.Join([]string{AuthMethodBasic, AuthMethodBearer}, ", ")
authMethodHelp := fmt.Sprintf("API authentication type (%s)", authTypes)
authTokenHelp := "API bearer access token"
authIDHelp := "Name of a file that contains the API access token"
// The caller can set an authType directly; otherwise, we will set
// the "APIAuthMethod" option and allow the user to choose..
if len(args) > 1 {
return Error("SetAPIOptions: bad call (too many args)")
} else if len(args) == 1 {
authType = args[0]
} else {
SetStringOpt("APIAuthMethod", "", true, AuthMethodBasic, authMethodHelp)
}
if authType == "" || authType == AuthMethodBasic {
authTokenHelp = "API access token (username:password)"
SetStringOpt("Username", "", true, "", "Username for API access")
SetStringOpt("Password", "", true, "", "Password for API access")
SetBoolOpt("PromptForPassword", "", false, false, "Prompt for password for API access")
}
// Set common API options:
SetStringOpt("APIBaseURL", "", true, "", "API base URL")
SetStringOpt("APIAuthToken", "", true, "", authTokenHelp)
SetStringOpt("APIAuthTokenID", "", true, "", authIDHelp)
SetStringOpt("SecretsDir", "", true, "", "Location of \"APIAuthTokenID\"")
SetIntOpt("HTTPTimeout", "", true, 60, "Timeout in seconds of the HTTP connection")
// This one is not really common, being used by only a few commands. For simplicity,
// though, I'll allow it everywhere and just ignore it if it does not apply.
SetStringOpt("GridReference", "", true, "", "specify the grid reference ID (to save a lookup)")
return nil
}
// ShowBody attempts to "pretty print" the returned body of an API request.
// It is generally used for debugging purposes.
// The old Mulesoft-based API returned a body as indented JSON,
// but the new java-based API returns normal (non-indented) JSON.
// The mailhome API endpoint returns a string.
func ShowBody(body []byte, indentOpts ...string) error {
prefix := ""
indent := " "
if len(body) == 0 {
return Error("body has no bytes")
} else if len(indentOpts) > 2 {
return Error("bug: bad call to ShowBody()")
}
if len(indentOpts) > 0 {
prefix = indentOpts[0]
}
if len(indentOpts) > 1 {
indent = indentOpts[1]
}
// If the body starts with a '[' or '{', assume it's a JSON sequence.
if body[0] == 91 || body[0] == 123 {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, body, prefix, indent)
if err != nil {
Warn("Failure indenting body for printing: %v", err)
Print("%s\n", body)
}
Fprint(DefaultShow, "%s\n", prettyJSON.Bytes())
return err
}
Print("%s\n", body)
return nil
}