forked from goadesign/goa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.go
352 lines (324 loc) · 10.3 KB
/
client.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
package goa
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"os"
"strings"
"time"
"github.com/spf13/cobra"
)
type (
// Client is the command client data structure for all goa service clients.
Client struct {
// Logger is the logger used to log client requests.
Logger
// Client is the underlying http client.
*http.Client
// Signers contains the ordered list of request signers. A signer may add headers,
// cookies etc. to a request generally to perform auth.
Signers []Signer
// Scheme is the HTTP scheme used to make requests to the API host.
Scheme string
// Host is the service hostname.
Host string
// UserAgent is the user agent set in requests made by the client.
UserAgent string
// Dump indicates whether to dump request response.
Dump bool
}
// Signer is the common interface implemented by all signers.
Signer interface {
// Sign adds required headers, cookies etc.
Sign(*http.Request) error
// RegisterFlags registers the command line flags that defines the values used to
// initialize the signer.
RegisterFlags(cmd *cobra.Command)
}
// BasicSigner implements basic auth.
BasicSigner struct {
// Username is the basic auth user.
Username string
// Password is err guess what? the basic auth password.
Password string
}
// JWTSigner implements JSON Web Token auth.
JWTSigner struct {
// Header is the name of the HTTP header which contains the JWT.
// The default is "Authentication"
Header string
// Format represents the format used to render the JWT.
// The default is "Bearer %s"
Format string
// token stores the actual JWT.
token string
}
// OAuth2Signer enables the use of OAuth2 refresh tokens. It takes care of creating access
// tokens given a refresh token and a refresh URL as defined in RFC 6749.
// Note that this signer does not concern itself with generating the initial refresh token,
// this has to be done prior to using the client.
// Also it assumes the response of the refresh request response is JSON encoded and of the
// form:
// {
// "access_token":"2YotnFZFEjr1zCsicMWpAA",
// "expires_in":3600,
// "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
// }
// where the "expires_in" and "refresh_token" properties are optional and additional
// properties are ignored. If the response contains a "expires_in" property then the signer
// takes care of making refresh requests prior to the token expiration.
OAuth2Signer struct {
// RefreshURLFormat is a format that generates the refresh access token URL given a
// refresh token.
RefreshURLFormat string
// RefreshToken contains the OAuth2 refresh token from which access tokens are
// created.
RefreshToken string
// accessToken is the temporary access token.
accessToken string
// expiresAt specifies when to create a new access token.
expiresAt time.Time
}
)
// NewClient create a new API client.
func NewClient() *Client {
return &Client{
Logger: &DefaultLogger{Logger: log.New(os.Stderr, "", log.LstdFlags)},
Client: http.DefaultClient,
}
}
// Do wraps the underlying http client Do method and adds logging.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", c.UserAgent)
var reqBody []byte
startedAt := time.Now()
id := shortID()
if c.Dump {
startedAt = time.Now()
reqBody = c.dumpRequest(req)
} else {
c.Info(nil, "started", KV{"id", id}, KV{req.Method, req.URL.String()})
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
if c.Dump {
c.dumpResponse(resp, req, reqBody)
} else {
c.Info(nil, "completed", KV{"id", id}, KV{"status", resp.StatusCode}, KV{"time", time.Since(startedAt).String()})
}
return resp, err
}
// Sign adds the basic auth header to the request.
func (s *BasicSigner) Sign(req *http.Request) error {
if s.Username != "" && s.Password != "" {
req.SetBasicAuth(s.Username, s.Password)
}
return nil
}
// RegisterFlags adds the "--user" and "--pass" flags to the client tool.
func (s *BasicSigner) RegisterFlags(app *cobra.Command) {
app.Flags().StringVar(&s.Username, "user", "", "Basic Auth username")
app.Flags().StringVar(&s.Password, "pass", "", "Basic Auth password")
}
// Sign adds the JWT auth header.
func (s *JWTSigner) Sign(req *http.Request) error {
header := s.Header
if header == "" {
header = "Authorization"
}
format := s.Format
if format == "" {
format = "Bearer %s"
}
req.Header.Set(header, fmt.Sprintf(format, s.token))
return nil
}
// RegisterFlags adds the "--jwt" flag to the client tool.
func (s *JWTSigner) RegisterFlags(app *cobra.Command) {
app.Flags().StringVar(&s.token, "jwt", "", "JSON web token")
}
// Sign refreshes the access token if needed and adds the OAuth header.
func (s *OAuth2Signer) Sign(req *http.Request) error {
if s.expiresAt.Before(time.Now()) {
if err := s.Refresh(); err != nil {
return fmt.Errorf("failed to refresh OAuth token: %s", err)
}
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken))
return nil
}
// RegisterFlags adds the "--refreshURL" and "--refreshToken" flags to the client tool.
func (s *OAuth2Signer) RegisterFlags(app *cobra.Command) {
app.Flags().StringVar(&s.RefreshURLFormat, "refreshURL", "", "OAuth2 refresh URL format, e.g. https://somewhere.com/token?grant_type=authorization_code&code=%s&client_id=xxx")
app.Flags().StringVar(&s.RefreshToken, "refreshToken", "", "OAuth2 refresh token or authorization code")
}
// ouath2RefreshResponse is the data structure representing the interesting subset of a OAuth2
// refresh response.
type oauth2RefreshResponse struct {
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
AccessToken string `json:"access_token"`
}
// Refresh makes a OAuth2 refresh access token request.
func (s *OAuth2Signer) Refresh() error {
url := fmt.Sprintf(s.RefreshURLFormat, s.RefreshToken)
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var r oauth2RefreshResponse
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %s", err)
}
err = json.Unmarshal(respBody, &r)
if err != nil {
return fmt.Errorf("failed to decode refresh request response: %s", err)
}
s.accessToken = r.AccessToken
if r.ExpiresIn > 0 {
s.expiresAt = time.Now().Add(time.Duration(r.ExpiresIn) * time.Second)
}
if r.RefreshToken != "" {
s.RefreshToken = r.RefreshToken
}
return nil
}
// Dump request if needed.
func (c *Client) dumpRequest(req *http.Request) []byte {
reqBody, err := dumpReqBody(req)
if err != nil {
c.Error(nil, "Failed to load request body for dump", KV{"err", err.Error()})
}
var buffer bytes.Buffer
buffer.WriteString(req.Method + " " + req.URL.String() + "\n")
writeHeaders(&buffer, req.Header)
if reqBody != nil {
buffer.WriteString("\n")
buffer.Write(reqBody)
buffer.WriteString("\n")
}
fmt.Fprint(os.Stderr, buffer.String())
return nil
}
// dumpResponse dumps the response and the request.
func (c *Client) dumpResponse(resp *http.Response, req *http.Request, reqBody []byte) {
respBody, _ := dumpRespBody(resp)
var buffer bytes.Buffer
buffer.WriteString("==> " + resp.Proto + " " + resp.Status + "\n")
writeHeaders(&buffer, resp.Header)
if respBody != nil {
buffer.WriteString("\n")
buffer.Write(respBody)
buffer.WriteString("\n")
}
fmt.Fprint(os.Stderr, buffer.String())
}
// writeHeaders is a helper function that writes the given HTTP headers to the given buffer as
// human readable strings. writeHeaders filters out headers that are sensitive.
func writeHeaders(buffer *bytes.Buffer, headers http.Header) {
filterHeaders(headers, func(name string, value []string) {
buffer.WriteString(name)
buffer.WriteString(": ")
buffer.WriteString(strings.Join(value, ", "))
buffer.WriteString("\n")
})
}
// Dump request body, strongly inspired from httputil.DumpRequest
func dumpReqBody(req *http.Request) ([]byte, error) {
if req.Body == nil {
return nil, nil
}
var save io.ReadCloser
var err error
save, req.Body, err = drainBody(req.Body)
if err != nil {
return nil, err
}
var b bytes.Buffer
var dest io.Writer = &b
chunked := len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked"
if chunked {
dest = httputil.NewChunkedWriter(dest)
}
_, err = io.Copy(dest, req.Body)
if chunked {
dest.(io.Closer).Close()
io.WriteString(&b, "\r\n")
}
req.Body = save
return b.Bytes(), err
}
// Dump response body, strongly inspired from httputil.DumpResponse
func dumpRespBody(resp *http.Response) ([]byte, error) {
if resp.Body == nil {
return nil, nil
}
var b bytes.Buffer
savecl := resp.ContentLength
var save io.ReadCloser
var err error
save, resp.Body, err = drainBody(resp.Body)
if err != nil {
return nil, err
}
_, err = io.Copy(&b, resp.Body)
if err != nil {
return nil, err
}
resp.Body = save
resp.ContentLength = savecl
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// One of the copies, say from b to r2, could be avoided by using a more
// elaborate trick where the other copy is made during Request/Response.Write.
// This would complicate things too much, given that these functions are for
// debugging only.
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
var buf bytes.Buffer
if _, err = buf.ReadFrom(b); err != nil {
return nil, nil, err
}
if err = b.Close(); err != nil {
return nil, nil, err
}
return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
}
// headerIterator is a HTTP header iterator.
type headerIterator func(name string, value []string)
// filterHeaders iterates through the headers skipping hidden headers.
// It calls the given iterator for each header name/value pair. The values are serialized as
// strings.
func filterHeaders(headers http.Header, iterator headerIterator) {
for k, v := range headers {
// Skip sensitive headers
if k == "Authorization" || k == "Cookie" {
continue
}
iterator(k, v)
}
}
// shortID produces a "unique" 6 bytes long string.
// Do not use as a reliable way to get unique IDs, instead use for things like logging.
func shortID() string {
b := make([]byte, 6)
io.ReadFull(rand.Reader, b)
return base64.StdEncoding.EncodeToString(b)
}