github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+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("Connection", "close") 202 request.Header.Set("content-type", "application/json") 203 request.Header.Set("User-Agent", "go-cli "+version.VersionString()+" / "+runtime.GOOS) 204 205 return &Request{HTTPReq: request, SeekableBody: body} 206 } 207 208 func (gateway Gateway) NewRequestForFile(method, fullURL, accessToken string, body *os.File) (*Request, error) { 209 progressReader := NewProgressReader(body, gateway.ui, 5*time.Second) 210 _, _ = progressReader.Seek(0, 0) 211 212 fileStats, err := body.Stat() 213 if err != nil { 214 return nil, fmt.Errorf("%s: %s", T("Error getting file info"), err.Error()) 215 } 216 217 request, err := http.NewRequest(method, fullURL, progressReader) 218 if err != nil { 219 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 220 } 221 222 fileSize := fileStats.Size() 223 progressReader.SetTotalSize(fileSize) 224 request.ContentLength = fileSize 225 226 if err != nil { 227 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 228 } 229 230 return gateway.newRequest(request, accessToken, progressReader), nil 231 } 232 233 func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (*Request, error) { 234 request, err := http.NewRequest(method, path, body) 235 if err != nil { 236 return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error()) 237 } 238 return gateway.newRequest(request, accessToken, body), nil 239 } 240 241 func (gateway Gateway) PerformRequest(request *Request) (*http.Response, error) { 242 return gateway.doRequestHandlingAuth(request) 243 } 244 245 func (gateway Gateway) performRequestForResponseBytes(request *Request) ([]byte, http.Header, *http.Response, error) { 246 rawResponse, err := gateway.doRequestHandlingAuth(request) 247 if err != nil { 248 return nil, nil, rawResponse, err 249 } 250 defer rawResponse.Body.Close() 251 252 bytes, err := ioutil.ReadAll(rawResponse.Body) 253 if err != nil { 254 return bytes, nil, rawResponse, fmt.Errorf("%s: %s", T("Error reading response"), err.Error()) 255 } 256 257 return bytes, rawResponse.Header, rawResponse, nil 258 } 259 260 func (gateway Gateway) PerformRequestForTextResponse(request *Request) (string, http.Header, error) { 261 bytes, headers, _, err := gateway.performRequestForResponseBytes(request) 262 return string(bytes), headers, err 263 } 264 265 func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (http.Header, error) { 266 bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request) 267 if err != nil { 268 if rawResponse != nil && rawResponse.Body != nil { 269 b, _ := ioutil.ReadAll(rawResponse.Body) 270 _ = json.Unmarshal(b, &response) 271 } 272 return headers, err 273 } 274 275 if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { 276 return headers, nil 277 } 278 279 err = json.Unmarshal(bytes, &response) 280 if err != nil { 281 return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error()) 282 } 283 284 return headers, nil 285 } 286 287 func (gateway Gateway) PerformPollingRequestForJSONResponse(endpoint string, request *Request, response interface{}, timeout time.Duration) (http.Header, error) { 288 query := request.HTTPReq.URL.Query() 289 query.Add("async", "true") 290 request.HTTPReq.URL.RawQuery = query.Encode() 291 292 bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request) 293 if err != nil { 294 return headers, err 295 } 296 defer rawResponse.Body.Close() 297 298 if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" { 299 return headers, nil 300 } 301 302 err = json.Unmarshal(bytes, &response) 303 if err != nil { 304 return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error()) 305 } 306 307 asyncResource := &AsyncResource{} 308 err = json.Unmarshal(bytes, &asyncResource) 309 if err != nil { 310 return headers, fmt.Errorf("%s: %s", T("Invalid async response from server"), err.Error()) 311 } 312 313 jobURL := asyncResource.Metadata.URL 314 if jobURL == "" { 315 return headers, nil 316 } 317 318 if !strings.Contains(jobURL, "/jobs/") { 319 return headers, nil 320 } 321 322 err = gateway.waitForJob(endpoint+jobURL, request.HTTPReq.Header.Get("Authorization"), timeout) 323 324 return headers, err 325 } 326 327 func (gateway Gateway) Warnings() []string { 328 return *gateway.warnings 329 } 330 331 func (gateway Gateway) waitForJob(jobURL, accessToken string, timeout time.Duration) error { 332 startTime := gateway.Clock() 333 for true { 334 if gateway.Clock().Sub(startTime) > timeout && timeout != 0 { 335 return errors.NewAsyncTimeoutError(jobURL) 336 } 337 var request *Request 338 request, err := gateway.NewRequest("GET", jobURL, accessToken, nil) 339 response := &JobResource{} 340 _, err = gateway.PerformRequestForJSONResponse(request, response) 341 if err != nil { 342 return err 343 } 344 345 switch response.Entity.Status { 346 case JobFinished: 347 return nil 348 case JobFailed: 349 return errors.New(response.Entity.ErrorDetails.Description) 350 } 351 352 accessToken = request.HTTPReq.Header.Get("Authorization") 353 354 time.Sleep(gateway.PollingThrottle) 355 } 356 return nil 357 } 358 359 func (gateway Gateway) doRequestHandlingAuth(request *Request) (*http.Response, error) { 360 httpReq := request.HTTPReq 361 362 if request.SeekableBody != nil { 363 httpReq.Body = ioutil.NopCloser(request.SeekableBody) 364 } 365 366 // perform request 367 rawResponse, err := gateway.doRequestAndHandlerError(request) 368 if err == nil || gateway.authenticator == nil { 369 return rawResponse, err 370 } 371 372 switch err.(type) { 373 case *errors.InvalidTokenError: 374 // refresh the auth token 375 var newToken string 376 newToken, err = gateway.authenticator.RefreshAuthToken() 377 if err != nil { 378 return rawResponse, err 379 } 380 381 // reset the auth token and request body 382 httpReq.Header.Set("Authorization", newToken) 383 if request.SeekableBody != nil { 384 _, _ = request.SeekableBody.Seek(0, 0) 385 httpReq.Body = ioutil.NopCloser(request.SeekableBody) 386 } 387 388 // make the request again 389 rawResponse, err = gateway.doRequestAndHandlerError(request) 390 } 391 392 return rawResponse, err 393 } 394 395 func (gateway Gateway) doRequestAndHandlerError(request *Request) (*http.Response, error) { 396 rawResponse, err := gateway.doRequest(request.HTTPReq) 397 if err != nil { 398 return rawResponse, WrapNetworkErrors(request.HTTPReq.URL.Host, err) 399 } 400 401 if rawResponse.StatusCode > 299 { 402 defer rawResponse.Body.Close() 403 jsonBytes, _ := ioutil.ReadAll(rawResponse.Body) 404 rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes)) 405 err = gateway.errHandler(rawResponse.StatusCode, jsonBytes) 406 } 407 408 return rawResponse, err 409 } 410 411 func (gateway Gateway) doRequest(request *http.Request) (*http.Response, error) { 412 var response *http.Response 413 var err error 414 415 if gateway.transport == nil { 416 makeHTTPTransport(&gateway) 417 } 418 419 httpClient := NewHTTPClient(gateway.transport, NewRequestDumper(gateway.logger)) 420 421 httpClient.DumpRequest(request) 422 423 for i := 0; i < 3; i++ { 424 response, err = httpClient.Do(request) 425 if response == nil && err != nil { 426 continue 427 } else { 428 break 429 } 430 } 431 432 if err != nil { 433 return response, err 434 } 435 436 httpClient.DumpResponse(response) 437 438 header := http.CanonicalHeaderKey("X-Cf-Warnings") 439 rawWarnings := response.Header[header] 440 for _, rawWarning := range rawWarnings { 441 warning, _ := url.QueryUnescape(rawWarning) 442 *gateway.warnings = append(*gateway.warnings, warning) 443 } 444 445 return response, err 446 } 447 448 func makeHTTPTransport(gateway *Gateway) { 449 gateway.transport = &http.Transport{ 450 Dial: (&net.Dialer{ 451 KeepAlive: 30 * time.Second, 452 Timeout: gateway.DialTimeout, 453 }).Dial, 454 TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()), 455 Proxy: http.ProxyFromEnvironment, 456 } 457 } 458 459 func dialTimeout(envDialTimeout string) time.Duration { 460 dialTimeout := DefaultDialTimeout 461 if timeout, err := strconv.Atoi(envDialTimeout); err == nil { 462 dialTimeout = time.Duration(timeout) * time.Second 463 } 464 return dialTimeout 465 } 466 467 func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) { 468 gateway.trustedCerts = certificates 469 makeHTTPTransport(gateway) 470 }