github.com/arunkumar7540/cli@v6.45.0+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 RefreshToken(token string) (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 if gateway.authenticator != nil { 366 authHeader := request.HTTPReq.Header.Get("Authorization") 367 // in case the request is purposefully unauthenticated (invocation without prior login) 368 // do not attempt to refresh the token (it does not exist) 369 if authHeader != "" { 370 token, err := gateway.authenticator.RefreshToken(authHeader) 371 if err != nil { 372 return nil, err 373 } 374 // if the token has not been refreshed, token is equivalent to the previously set header value 375 httpReq.Header.Set("Authorization", token) 376 } 377 } 378 379 // perform request 380 return gateway.doRequestAndHandlerError(request) 381 } 382 383 func (gateway Gateway) doRequestAndHandlerError(request *Request) (*http.Response, error) { 384 rawResponse, err := gateway.doRequest(request.HTTPReq) 385 if err != nil { 386 return rawResponse, WrapNetworkErrors(request.HTTPReq.URL.Host, err) 387 } 388 389 if rawResponse.StatusCode > 299 { 390 defer rawResponse.Body.Close() 391 jsonBytes, _ := ioutil.ReadAll(rawResponse.Body) 392 rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes)) 393 err = gateway.errHandler(rawResponse.StatusCode, jsonBytes) 394 } 395 396 return rawResponse, err 397 } 398 399 func (gateway Gateway) doRequest(request *http.Request) (*http.Response, error) { 400 var response *http.Response 401 var err error 402 403 if gateway.transport == nil { 404 makeHTTPTransport(&gateway) 405 } 406 407 httpClient := NewHTTPClient(gateway.transport, NewRequestDumper(gateway.logger)) 408 409 httpClient.DumpRequest(request) 410 411 for i := 0; i < 3; i++ { 412 response, err = httpClient.Do(request) 413 if response == nil && err != nil { 414 continue 415 } else { 416 break 417 } 418 } 419 420 if err != nil { 421 return response, err 422 } 423 424 httpClient.DumpResponse(response) 425 426 rawWarnings := strings.Split(response.Header.Get("X-Cf-Warnings"), ",") 427 for _, rawWarning := range rawWarnings { 428 if rawWarning == "" { 429 continue 430 } 431 warning, _ := url.QueryUnescape(rawWarning) 432 *gateway.warnings = append(*gateway.warnings, warning) 433 } 434 435 return response, err 436 } 437 438 func makeHTTPTransport(gateway *Gateway) { 439 gateway.transport = &http.Transport{ 440 DisableKeepAlives: true, 441 Dial: (&net.Dialer{ 442 KeepAlive: 30 * time.Second, 443 Timeout: gateway.DialTimeout, 444 }).Dial, 445 TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()), 446 Proxy: http.ProxyFromEnvironment, 447 } 448 } 449 450 func dialTimeout(envDialTimeout string) time.Duration { 451 dialTimeout := DefaultDialTimeout 452 if timeout, err := strconv.Atoi(envDialTimeout); err == nil { 453 dialTimeout = time.Duration(timeout) * time.Second 454 } 455 return dialTimeout 456 } 457 458 func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) { 459 gateway.trustedCerts = certificates 460 makeHTTPTransport(gateway) 461 }