1package gophercloud 2 3import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "strings" 10 "sync" 11) 12 13// DefaultUserAgent is the default User-Agent string set in the request header. 14const DefaultUserAgent = "gophercloud/2.0.0" 15 16// UserAgent represents a User-Agent header. 17type UserAgent struct { 18 // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. 19 // All the strings to prepend are accumulated and prepended in the Join method. 20 prepend []string 21} 22 23// Prepend prepends a user-defined string to the default User-Agent string. Users 24// may pass in one or more strings to prepend. 25func (ua *UserAgent) Prepend(s ...string) { 26 ua.prepend = append(s, ua.prepend...) 27} 28 29// Join concatenates all the user-defined User-Agend strings with the default 30// Gophercloud User-Agent string. 31func (ua *UserAgent) Join() string { 32 uaSlice := append(ua.prepend, DefaultUserAgent) 33 return strings.Join(uaSlice, " ") 34} 35 36// ProviderClient stores details that are required to interact with any 37// services within a specific provider's API. 38// 39// Generally, you acquire a ProviderClient by calling the NewClient method in 40// the appropriate provider's child package, providing whatever authentication 41// credentials are required. 42type ProviderClient struct { 43 // IdentityBase is the base URL used for a particular provider's identity 44 // service - it will be used when issuing authenticatation requests. It 45 // should point to the root resource of the identity service, not a specific 46 // identity version. 47 IdentityBase string 48 49 // IdentityEndpoint is the identity endpoint. This may be a specific version 50 // of the identity service. If this is the case, this endpoint is used rather 51 // than querying versions first. 52 IdentityEndpoint string 53 54 // TokenID is the ID of the most recently issued valid token. 55 // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. 56 // To safely read or write this value, call `Token` or `SetToken`, respectively 57 TokenID string 58 59 // EndpointLocator describes how this provider discovers the endpoints for 60 // its constituent services. 61 EndpointLocator EndpointLocator 62 63 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. 64 HTTPClient http.Client 65 66 // UserAgent represents the User-Agent header in the HTTP request. 67 UserAgent UserAgent 68 69 // ReauthFunc is the function used to re-authenticate the user if the request 70 // fails with a 401 HTTP response code. This a needed because there may be multiple 71 // authentication functions for different Identity service versions. 72 ReauthFunc func() error 73 74 mut *sync.RWMutex 75 76 reauthmut *reauthlock 77} 78 79type reauthlock struct { 80 sync.RWMutex 81 reauthing bool 82} 83 84// AuthenticatedHeaders returns a map of HTTP headers that are common for all 85// authenticated service requests. 86func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { 87 if client.reauthmut != nil { 88 client.reauthmut.RLock() 89 if client.reauthmut.reauthing { 90 client.reauthmut.RUnlock() 91 return 92 } 93 client.reauthmut.RUnlock() 94 } 95 t := client.Token() 96 if t == "" { 97 return 98 } 99 return map[string]string{"X-Auth-Token": t} 100} 101 102// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. 103// If the application's ProviderClient is not used concurrently, this doesn't need to be called. 104func (client *ProviderClient) UseTokenLock() { 105 client.mut = new(sync.RWMutex) 106 client.reauthmut = new(reauthlock) 107} 108 109// Token safely reads the value of the auth token from the ProviderClient. Applications should 110// call this method to access the token instead of the TokenID field 111func (client *ProviderClient) Token() string { 112 if client.mut != nil { 113 client.mut.RLock() 114 defer client.mut.RUnlock() 115 } 116 return client.TokenID 117} 118 119// SetToken safely sets the value of the auth token in the ProviderClient. Applications may 120// use this method in a custom ReauthFunc 121func (client *ProviderClient) SetToken(t string) { 122 if client.mut != nil { 123 client.mut.Lock() 124 defer client.mut.Unlock() 125 } 126 client.TokenID = t 127} 128 129//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is 130//called because of a 401 response, the caller may pass the previous token. In 131//this case, the reauthentication can be skipped if another thread has already 132//reauthenticated in the meantime. If no previous token is known, an empty 133//string should be passed instead to force unconditional reauthentication. 134func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { 135 if client.ReauthFunc == nil { 136 return nil 137 } 138 139 if client.mut == nil { 140 return client.ReauthFunc() 141 } 142 client.mut.Lock() 143 defer client.mut.Unlock() 144 145 client.reauthmut.Lock() 146 client.reauthmut.reauthing = true 147 client.reauthmut.Unlock() 148 149 if previousToken == "" || client.TokenID == previousToken { 150 err = client.ReauthFunc() 151 } 152 153 client.reauthmut.Lock() 154 client.reauthmut.reauthing = false 155 client.reauthmut.Unlock() 156 return 157} 158 159// RequestOpts customizes the behavior of the provider.Request() method. 160type RequestOpts struct { 161 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The 162 // content type of the request will default to "application/json" unless overridden by MoreHeaders. 163 // It's an error to specify both a JSONBody and a RawBody. 164 JSONBody interface{} 165 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type 166 // will be set unless one is provided explicitly by MoreHeaders. 167 RawBody io.Reader 168 // JSONResponse, if provided, will be populated with the contents of the response body parsed as 169 // JSON. 170 JSONResponse interface{} 171 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If 172 // the response has a different code, an error will be returned. 173 OkCodes []int 174 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is 175 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress 176 // the default Accept header or an inferred Content-Type, for example. 177 MoreHeaders map[string]string 178 // ErrorContext specifies the resource error type to return if an error is encountered. 179 // This lets resources override default error messages based on the response status code. 180 ErrorContext error 181} 182 183var applicationJSON = "application/json" 184 185// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication 186// header will automatically be provided. 187func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { 188 var body io.Reader 189 var contentType *string 190 191 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided 192 // io.ReadSeeker as-is. Default the content-type to application/json. 193 if options.JSONBody != nil { 194 if options.RawBody != nil { 195 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") 196 } 197 198 rendered, err := json.Marshal(options.JSONBody) 199 if err != nil { 200 return nil, err 201 } 202 203 body = bytes.NewReader(rendered) 204 contentType = &applicationJSON 205 } 206 207 if options.RawBody != nil { 208 body = options.RawBody 209 } 210 211 // Construct the http.Request. 212 req, err := http.NewRequest(method, url, body) 213 if err != nil { 214 return nil, err 215 } 216 217 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to 218 // modify or omit any header. 219 if contentType != nil { 220 req.Header.Set("Content-Type", *contentType) 221 } 222 req.Header.Set("Accept", applicationJSON) 223 224 // Set the User-Agent header 225 req.Header.Set("User-Agent", client.UserAgent.Join()) 226 227 if options.MoreHeaders != nil { 228 for k, v := range options.MoreHeaders { 229 if v != "" { 230 req.Header.Set(k, v) 231 } else { 232 req.Header.Del(k) 233 } 234 } 235 } 236 237 // get latest token from client 238 for k, v := range client.AuthenticatedHeaders() { 239 req.Header.Set(k, v) 240 } 241 242 // Set connection parameter to close the connection immediately when we've got the response 243 req.Close = true 244 245 prereqtok := req.Header.Get("X-Auth-Token") 246 247 // Issue the request. 248 resp, err := client.HTTPClient.Do(req) 249 if err != nil { 250 return nil, err 251 } 252 253 // Allow default OkCodes if none explicitly set 254 if options.OkCodes == nil { 255 options.OkCodes = defaultOkCodes(method) 256 } 257 258 // Validate the HTTP response status. 259 var ok bool 260 for _, code := range options.OkCodes { 261 if resp.StatusCode == code { 262 ok = true 263 break 264 } 265 } 266 267 if !ok { 268 body, _ := ioutil.ReadAll(resp.Body) 269 resp.Body.Close() 270 respErr := ErrUnexpectedResponseCode{ 271 URL: url, 272 Method: method, 273 Expected: options.OkCodes, 274 Actual: resp.StatusCode, 275 Body: body, 276 } 277 278 errType := options.ErrorContext 279 switch resp.StatusCode { 280 case http.StatusBadRequest: 281 err = ErrDefault400{respErr} 282 if error400er, ok := errType.(Err400er); ok { 283 err = error400er.Error400(respErr) 284 } 285 case http.StatusUnauthorized: 286 if client.ReauthFunc != nil { 287 err = client.Reauthenticate(prereqtok) 288 if err != nil { 289 e := &ErrUnableToReauthenticate{} 290 e.ErrOriginal = respErr 291 return nil, e 292 } 293 if options.RawBody != nil { 294 if seeker, ok := options.RawBody.(io.Seeker); ok { 295 seeker.Seek(0, 0) 296 } 297 } 298 // make a new call to request with a nil reauth func in order to avoid infinite loop 299 reauthFunc := client.ReauthFunc 300 client.ReauthFunc = nil 301 resp, err = client.Request(method, url, options) 302 client.ReauthFunc = reauthFunc 303 if err != nil { 304 switch err.(type) { 305 case *ErrUnexpectedResponseCode: 306 e := &ErrErrorAfterReauthentication{} 307 e.ErrOriginal = err.(*ErrUnexpectedResponseCode) 308 return nil, e 309 default: 310 e := &ErrErrorAfterReauthentication{} 311 e.ErrOriginal = err 312 return nil, e 313 } 314 } 315 return resp, nil 316 } 317 err = ErrDefault401{respErr} 318 if error401er, ok := errType.(Err401er); ok { 319 err = error401er.Error401(respErr) 320 } 321 case http.StatusForbidden: 322 err = ErrDefault403{respErr} 323 if error403er, ok := errType.(Err403er); ok { 324 err = error403er.Error403(respErr) 325 } 326 case http.StatusNotFound: 327 err = ErrDefault404{respErr} 328 if error404er, ok := errType.(Err404er); ok { 329 err = error404er.Error404(respErr) 330 } 331 case http.StatusMethodNotAllowed: 332 err = ErrDefault405{respErr} 333 if error405er, ok := errType.(Err405er); ok { 334 err = error405er.Error405(respErr) 335 } 336 case http.StatusRequestTimeout: 337 err = ErrDefault408{respErr} 338 if error408er, ok := errType.(Err408er); ok { 339 err = error408er.Error408(respErr) 340 } 341 case 429: 342 err = ErrDefault429{respErr} 343 if error429er, ok := errType.(Err429er); ok { 344 err = error429er.Error429(respErr) 345 } 346 case http.StatusInternalServerError: 347 err = ErrDefault500{respErr} 348 if error500er, ok := errType.(Err500er); ok { 349 err = error500er.Error500(respErr) 350 } 351 case http.StatusServiceUnavailable: 352 err = ErrDefault503{respErr} 353 if error503er, ok := errType.(Err503er); ok { 354 err = error503er.Error503(respErr) 355 } 356 } 357 358 if err == nil { 359 err = respErr 360 } 361 362 return resp, err 363 } 364 365 // Parse the response body as JSON, if requested to do so. 366 if options.JSONResponse != nil { 367 defer resp.Body.Close() 368 if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { 369 return nil, err 370 } 371 } 372 373 return resp, nil 374} 375 376func defaultOkCodes(method string) []int { 377 switch { 378 case method == "GET": 379 return []int{200} 380 case method == "POST": 381 return []int{201, 202} 382 case method == "PUT": 383 return []int{201, 202} 384 case method == "PATCH": 385 return []int{200, 202, 204} 386 case method == "DELETE": 387 return []int{202, 204} 388 } 389 390 return []int{} 391} 392