github.com/jenspinney/cli@v6.42.1-0.20190207184520-7450c600020e+incompatible/cf/net/gateway.go (about) 1 package net 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net" 11 "net/http" 12 "net/url" 13 "os" 14 "runtime" 15 "strconv" 16 "strings" 17 "time" 18 19 "code.cloudfoundry.org/cli/cf/configuration/coreconfig" 20 "code.cloudfoundry.org/cli/cf/errors" 21 . "code.cloudfoundry.org/cli/cf/i18n" 22 "code.cloudfoundry.org/cli/cf/terminal" 23 "code.cloudfoundry.org/cli/cf/trace" 24 "code.cloudfoundry.org/cli/version" 25 ) 26 27 const ( 28 JobFinished = "finished" 29 JobFailed = "failed" 30 DefaultPollingThrottle = 5 * time.Second 31 DefaultDialTimeout = 5 * time.Second 32 ) 33 34 type JobResource struct { 35 Entity struct { 36 Status string 37 ErrorDetails struct { 38 Description string 39 } `json:"error_details"` 40 } 41 } 42 43 type AsyncResource struct { 44 Metadata struct { 45 URL string 46 } 47 } 48 49 type apiErrorHandler func(statusCode int, body []byte) error 50 51 type tokenRefresher interface { 52 RefreshAuthToken() (string, error) 53 } 54 55 type Request struct { 56 HTTPReq *http.Request 57 SeekableBody io.ReadSeeker 58 } 59 60 type Gateway struct { 61 authenticator tokenRefresher 62 errHandler apiErrorHandler 63 PollingEnabled bool 64 PollingThrottle time.Duration 65 trustedCerts []tls.Certificate 66 config coreconfig.Reader 67 warnings *[]string 68 Clock func() time.Time 69 transport *http.Transport 70 ui terminal.UI 71 logger trace.Printer 72 DialTimeout time.Duration 73 } 74 75 func (gateway *Gateway) AsyncTimeout() time.Duration { 76 if gateway.config.AsyncTimeout() > 0 { 77 return time.Duration(gateway.config.AsyncTimeout()) * time.Minute 78 } 79 80 return 0 81 } 82 83 func (gateway *Gateway) SetTokenRefresher(auth tokenRefresher) { 84 gateway.authenticator = auth 85 } 86 87 func (gateway Gateway) GetResource(url string, resource interface{}) (err error) { 88 request, err := gateway.NewRequest("GET", url, gateway.config.AccessToken(), nil) 89 if err != nil { 90 return 91 } 92 93 _, err = gateway.PerformRequestForJSONResponse(request, resource) 94 return 95 } 96 97 func (gateway Gateway) CreateResourceFromStruct(endpoint, url string, resource interface{}) error { 98 data, err := json.Marshal(resource) 99 if err != nil { 100 return err 101 } 102 103 return gateway.CreateResource(endpoint, url, bytes.NewReader(data)) 104 } 105 106 func (gateway Gateway) UpdateResourceFromStruct(endpoint, apiURL string, resource interface{}) error { 107 data, err := json.Marshal(resource) 108 if err != nil { 109 return err 110 } 111 112 return gateway.UpdateResource(endpoint, apiURL, bytes.NewReader(data)) 113 } 114 115 func (gateway Gateway) CreateResource(endpoint, apiURL string, body io.ReadSeeker, resource ...interface{}) error { 116 return gateway.createUpdateOrDeleteResource("POST", endpoint, apiURL, body, false, resource...) 117 } 118 119 func (gateway Gateway) UpdateResource(endpoint, apiURL string, body io.ReadSeeker, resource ...interface{}) error { 120 return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiURL, body, false, resource...) 121 } 122 123 func (gateway Gateway) UpdateResourceSync(endpoint, apiURL string, body io.ReadSeeker, resource ...interface{}) error { 124 return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiURL, body, true, resource...) 125 } 126 127 func (gateway Gateway) DeleteResourceSynchronously(endpoint, apiURL string) error { 128 return gateway.createUpdateOrDeleteResource("DELETE", endpoint, apiURL, nil, true, &AsyncResource{}) 129 } 130 131 func (gateway Gateway) DeleteResource(endpoint, apiURL string) error { 132 return gateway.createUpdateOrDeleteResource("DELETE", endpoint, apiURL, nil, false, &AsyncResource{}) 133 } 134 135 func (gateway Gateway) ListPaginatedResources( 136 target string, 137 path string, 138 resource interface{}, 139 cb func(interface{}) bool, 140 ) error { 141 for path != "" { 142 pagination := NewPaginatedResources(resource) 143 144 apiErr := gateway.GetResource(fmt.Sprintf("%s%s", target, path), &pagination) 145 if apiErr != nil { 146 return apiErr 147 } 148 149 resources, err := pagination.Resources() 150 if err != nil { 151 return fmt.Errorf("%s: %s", T("Error parsing JSON"), err.Error()) 152 } 153 154 for _, resource := range resources { 155 if !cb(resource) { 156 return nil 157 } 158 } 159 160 path = pagination.NextURL 161 } 162 163 return nil 164 } 165 166 func (gateway Gateway) createUpdateOrDeleteResource(verb, endpoint, apiURL string, body io.ReadSeeker, sync bool, optionalResource ...interface{}) error { 167 var resource interface{} 168 if len(optionalResource) > 0 { 169 resource = optionalResource[0] 170 } 171 172 request, err := gateway.NewRequest(verb, endpoint+apiURL, gateway.config.AccessToken(), body) 173 if err != nil { 174 return err 175 } 176 177 if resource == nil { 178 _, err = gateway.PerformRequest(request) 179 return err 180 } 181 182 if gateway.PollingEnabled && !sync { 183 _, err = gateway.PerformPollingRequestForJSONResponse(endpoint, request, resource, gateway.AsyncTimeout()) 184 return err 185 } 186 187 _, err = gateway.PerformRequestForJSONResponse(request, resource) 188 if err != nil { 189 return err 190 } 191 192 return nil 193 } 194 195 func (gateway Gateway) newRequest(request *http.Request, accessToken string, body io.ReadSeeker) *Request { 196 if accessToken != "" { 197 request.Header.Set("Authorization", accessToken) 198 } 199 200 request.Header.Set("accept", "application/json") 201 request.Header.Set("content-type", "application/json") 202 request.Header.Set("User-Agent", "go-cli "+version.VersionString()+" / "+runtime.GOOS) 203 204 return &Request{HTTPReq: request, SeekableBody: body} 205 } 206 207 func (gateway Gateway) NewRequestForFile(method, fullURL, accessToken string, body *os.File) (*Request, error) { 208 progressReader := NewProgressReader(body, gateway.ui, 5*time.Second) 209 _, _ = progressReader.Seek(0, 0) 210 211 fileStats, err := body.Stat() 212 if err != nil { 213 return nil, fmt.Errorf("%s: %s", T("Error getting file info"), err.Error()) 214 } 215 216 request, err := http.NewRequest(method, fullURL, progressReader) 217 if err != nil { 218 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 219 } 220 221 fileSize := fileStats.Size() 222 progressReader.SetTotalSize(fileSize) 223 request.ContentLength = fileSize 224 225 if err != nil { 226 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 227 } 228 229 return gateway.newRequest(request, accessToken, progressReader), nil 230 } 231 232 func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (*Request, error) { 233 request, err := http.NewRequest(method, path, body) 234 if err != nil { 235 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 236 } 237 return gateway.newRequest(request, accessToken, body), nil 238 } 239 240 func (gateway Gateway) PerformRequest(request *Request) (*http.Response, error) { 241 return gateway.doRequestHandlingAuth(request) 242 } 243 244 func (gateway Gateway) performRequestForResponseBytes(request *Request) ([]byte, http.Header, *http.Response, error) { 245 rawResponse, err := gateway.doRequestHandlingAuth(request) 246 if err != nil { 247 return nil, nil, rawResponse, err 248 } 249 defer rawResponse.Body.Close() 250 251 bytes, err := ioutil.ReadAll(rawResponse.Body) 252 if err != nil { 253 return bytes, nil, rawResponse, fmt.Errorf("%s: %s", T("Error reading response"), err.Error()) 254 } 255 256 return bytes, rawResponse.Header, rawResponse, nil 257 } 258 259 func (gateway Gateway) PerformRequestForTextResponse(request *Request) (string, http.Header, error) { 260 bytes, headers, _, err := gateway.performRequestForResponseBytes(request) 261 return string(bytes), headers, err 262 } 263 264 func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (http.Header, error) { 265 bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request) 266 if err != nil { 267 if rawResponse != nil && rawResponse.Body != nil { 268 b, _ := ioutil.ReadAll(rawResponse.Body) 269 _ = json.Unmarshal(b, &response) 270 } 271 return headers, err 272 } 273 274 if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { 275 return headers, nil 276 } 277 278 err = json.Unmarshal(bytes, &response) 279 if err != nil { 280 return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error()) 281 } 282 283 return headers, nil 284 } 285 286 func (gateway Gateway) PerformPollingRequestForJSONResponse(endpoint string, request *Request, response interface{}, timeout time.Duration) (http.Header, error) { 287 query := request.HTTPReq.URL.Query() 288 query.Add("async", "true") 289 request.HTTPReq.URL.RawQuery = query.Encode() 290 291 bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request) 292 if err != nil { 293 return headers, err 294 } 295 defer rawResponse.Body.Close() 296 297 if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { 298 return headers, nil 299 } 300 301 err = json.Unmarshal(bytes, &response) 302 if err != nil { 303 return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error()) 304 } 305 306 asyncResource := &AsyncResource{} 307 err = json.Unmarshal(bytes, &asyncResource) 308 if err != nil { 309 return headers, fmt.Errorf("%s: %s", T("Invalid async response from server"), err.Error()) 310 } 311 312 jobURL := asyncResource.Metadata.URL 313 if jobURL == "" { 314 return headers, nil 315 } 316 317 if !strings.Contains(jobURL, "/jobs/") { 318 return headers, nil 319 } 320 321 err = gateway.waitForJob(endpoint+jobURL, request.HTTPReq.Header.Get("Authorization"), timeout) 322 323 return headers, err 324 } 325 326 func (gateway Gateway) Warnings() []string { 327 return *gateway.warnings 328 } 329 330 func (gateway Gateway) waitForJob(jobURL, accessToken string, timeout time.Duration) error { 331 startTime := gateway.Clock() 332 for true { 333 if gateway.Clock().Sub(startTime) > timeout && timeout != 0 { 334 return errors.NewAsyncTimeoutError(jobURL) 335 } 336 var request *Request 337 request, err := gateway.NewRequest("GET", jobURL, accessToken, nil) 338 response := &JobResource{} 339 _, err = gateway.PerformRequestForJSONResponse(request, response) 340 if err != nil { 341 return err 342 } 343 344 switch response.Entity.Status { 345 case JobFinished: 346 return nil 347 case JobFailed: 348 return errors.New(response.Entity.ErrorDetails.Description) 349 } 350 351 accessToken = request.HTTPReq.Header.Get("Authorization") 352 353 time.Sleep(gateway.PollingThrottle) 354 } 355 return nil 356 } 357 358 func (gateway Gateway) doRequestHandlingAuth(request *Request) (*http.Response, error) { 359 httpReq := request.HTTPReq 360 361 if request.SeekableBody != nil { 362 httpReq.Body = ioutil.NopCloser(request.SeekableBody) 363 } 364 365 // perform request 366 rawResponse, err := gateway.doRequestAndHandlerError(request) 367 if err == nil || gateway.authenticator == nil { 368 return rawResponse, err 369 } 370 371 switch err.(type) { 372 case *errors.InvalidTokenError: 373 // refresh the auth token 374 var newToken string 375 newToken, err = gateway.authenticator.RefreshAuthToken() 376 if err != nil { 377 return rawResponse, err 378 } 379 380 // reset the auth token and request body 381 httpReq.Header.Set("Authorization", newToken) 382 if request.SeekableBody != nil { 383 _, _ = request.SeekableBody.Seek(0, 0) 384 httpReq.Body = ioutil.NopCloser(request.SeekableBody) 385 } 386 387 // make the request again 388 rawResponse, err = gateway.doRequestAndHandlerError(request) 389 } 390 391 return rawResponse, err 392 } 393 394 func (gateway Gateway) doRequestAndHandlerError(request *Request) (*http.Response, error) { 395 rawResponse, err := gateway.doRequest(request.HTTPReq) 396 if err != nil { 397 return rawResponse, WrapNetworkErrors(request.HTTPReq.URL.Host, err) 398 } 399 400 if rawResponse.StatusCode > 299 { 401 defer rawResponse.Body.Close() 402 jsonBytes, _ := ioutil.ReadAll(rawResponse.Body) 403 rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes)) 404 err = gateway.errHandler(rawResponse.StatusCode, jsonBytes) 405 } 406 407 return rawResponse, err 408 } 409 410 func (gateway Gateway) doRequest(request *http.Request) (*http.Response, error) { 411 var response *http.Response 412 var err error 413 414 if gateway.transport == nil { 415 makeHTTPTransport(&gateway) 416 } 417 418 httpClient := NewHTTPClient(gateway.transport, NewRequestDumper(gateway.logger)) 419 420 httpClient.DumpRequest(request) 421 422 for i := 0; i < 3; i++ { 423 response, err = httpClient.Do(request) 424 if response == nil && err != nil { 425 continue 426 } else { 427 break 428 } 429 } 430 431 if err != nil { 432 return response, err 433 } 434 435 httpClient.DumpResponse(response) 436 437 rawWarnings := strings.Split(response.Header.Get("X-Cf-Warnings"), ",") 438 for _, rawWarning := range rawWarnings { 439 if rawWarning == "" { 440 continue 441 } 442 warning, _ := url.QueryUnescape(rawWarning) 443 *gateway.warnings = append(*gateway.warnings, warning) 444 } 445 446 return response, err 447 } 448 449 func makeHTTPTransport(gateway *Gateway) { 450 gateway.transport = &http.Transport{ 451 DisableKeepAlives: true, 452 Dial: (&net.Dialer{ 453 KeepAlive: 30 * time.Second, 454 Timeout: gateway.DialTimeout, 455 }).Dial, 456 TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()), 457 Proxy: http.ProxyFromEnvironment, 458 } 459 } 460 461 func dialTimeout(envDialTimeout string) time.Duration { 462 dialTimeout := DefaultDialTimeout 463 if timeout, err := strconv.Atoi(envDialTimeout); err == nil { 464 dialTimeout = time.Duration(timeout) * time.Second 465 } 466 return dialTimeout 467 } 468 469 func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) { 470 gateway.trustedCerts = certificates 471 makeHTTPTransport(gateway) 472 }