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