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