1// Package storage provides clients for Microsoft Azure Storage Services. 2package storage 3 4// Copyright 2017 Microsoft Corporation 5// 6// Licensed under the Apache License, Version 2.0 (the "License"); 7// you may not use this file except in compliance with the License. 8// You may obtain a copy of the License at 9// 10// http://www.apache.org/licenses/LICENSE-2.0 11// 12// Unless required by applicable law or agreed to in writing, software 13// distributed under the License is distributed on an "AS IS" BASIS, 14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15// See the License for the specific language governing permissions and 16// limitations under the License. 17 18import ( 19 "bufio" 20 "encoding/base64" 21 "encoding/json" 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "mime" 28 "mime/multipart" 29 "net/http" 30 "net/url" 31 "regexp" 32 "runtime" 33 "strconv" 34 "strings" 35 "time" 36 37 "github.com/Azure/azure-sdk-for-go/version" 38 "github.com/Azure/go-autorest/autorest" 39 "github.com/Azure/go-autorest/autorest/azure" 40) 41 42const ( 43 // DefaultBaseURL is the domain name used for storage requests in the 44 // public cloud when a default client is created. 45 DefaultBaseURL = "core.windows.net" 46 47 // DefaultAPIVersion is the Azure Storage API version string used when a 48 // basic client is created. 49 DefaultAPIVersion = "2016-05-31" 50 51 defaultUseHTTPS = true 52 defaultRetryAttempts = 5 53 defaultRetryDuration = time.Second * 5 54 55 // StorageEmulatorAccountName is the fixed storage account used by Azure Storage Emulator 56 StorageEmulatorAccountName = "devstoreaccount1" 57 58 // StorageEmulatorAccountKey is the the fixed storage account used by Azure Storage Emulator 59 StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" 60 61 blobServiceName = "blob" 62 tableServiceName = "table" 63 queueServiceName = "queue" 64 fileServiceName = "file" 65 66 storageEmulatorBlob = "127.0.0.1:10000" 67 storageEmulatorTable = "127.0.0.1:10002" 68 storageEmulatorQueue = "127.0.0.1:10001" 69 70 userAgentHeader = "User-Agent" 71 72 userDefinedMetadataHeaderPrefix = "x-ms-meta-" 73 74 connectionStringAccountName = "accountname" 75 connectionStringAccountKey = "accountkey" 76 connectionStringEndpointSuffix = "endpointsuffix" 77 connectionStringEndpointProtocol = "defaultendpointsprotocol" 78 79 connectionStringBlobEndpoint = "blobendpoint" 80 connectionStringFileEndpoint = "fileendpoint" 81 connectionStringQueueEndpoint = "queueendpoint" 82 connectionStringTableEndpoint = "tableendpoint" 83 connectionStringSAS = "sharedaccesssignature" 84) 85 86var ( 87 validStorageAccount = regexp.MustCompile("^[0-9a-z]{3,24}$") 88 defaultValidStatusCodes = []int{ 89 http.StatusRequestTimeout, // 408 90 http.StatusInternalServerError, // 500 91 http.StatusBadGateway, // 502 92 http.StatusServiceUnavailable, // 503 93 http.StatusGatewayTimeout, // 504 94 } 95) 96 97// Sender sends a request 98type Sender interface { 99 Send(*Client, *http.Request) (*http.Response, error) 100} 101 102// DefaultSender is the default sender for the client. It implements 103// an automatic retry strategy. 104type DefaultSender struct { 105 RetryAttempts int 106 RetryDuration time.Duration 107 ValidStatusCodes []int 108 attempts int // used for testing 109} 110 111// Send is the default retry strategy in the client 112func (ds *DefaultSender) Send(c *Client, req *http.Request) (resp *http.Response, err error) { 113 rr := autorest.NewRetriableRequest(req) 114 for attempts := 0; attempts < ds.RetryAttempts; attempts++ { 115 err = rr.Prepare() 116 if err != nil { 117 return resp, err 118 } 119 resp, err = c.HTTPClient.Do(rr.Request()) 120 if err != nil || !autorest.ResponseHasStatusCode(resp, ds.ValidStatusCodes...) { 121 return resp, err 122 } 123 drainRespBody(resp) 124 autorest.DelayForBackoff(ds.RetryDuration, attempts, req.Cancel) 125 ds.attempts = attempts 126 } 127 ds.attempts++ 128 return resp, err 129} 130 131// Client is the object that needs to be constructed to perform 132// operations on the storage account. 133type Client struct { 134 // HTTPClient is the http.Client used to initiate API 135 // requests. http.DefaultClient is used when creating a 136 // client. 137 HTTPClient *http.Client 138 139 // Sender is an interface that sends the request. Clients are 140 // created with a DefaultSender. The DefaultSender has an 141 // automatic retry strategy built in. The Sender can be customized. 142 Sender Sender 143 144 accountName string 145 accountKey []byte 146 useHTTPS bool 147 UseSharedKeyLite bool 148 baseURL string 149 apiVersion string 150 userAgent string 151 sasClient bool 152 accountSASToken url.Values 153} 154 155type odataResponse struct { 156 resp *http.Response 157 odata odataErrorWrapper 158} 159 160// AzureStorageServiceError contains fields of the error response from 161// Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx 162// Some fields might be specific to certain calls. 163type AzureStorageServiceError struct { 164 Code string `xml:"Code"` 165 Message string `xml:"Message"` 166 AuthenticationErrorDetail string `xml:"AuthenticationErrorDetail"` 167 QueryParameterName string `xml:"QueryParameterName"` 168 QueryParameterValue string `xml:"QueryParameterValue"` 169 Reason string `xml:"Reason"` 170 Lang string 171 StatusCode int 172 RequestID string 173 Date string 174 APIVersion string 175} 176 177type odataErrorMessage struct { 178 Lang string `json:"lang"` 179 Value string `json:"value"` 180} 181 182type odataError struct { 183 Code string `json:"code"` 184 Message odataErrorMessage `json:"message"` 185} 186 187type odataErrorWrapper struct { 188 Err odataError `json:"odata.error"` 189} 190 191// UnexpectedStatusCodeError is returned when a storage service responds with neither an error 192// nor with an HTTP status code indicating success. 193type UnexpectedStatusCodeError struct { 194 allowed []int 195 got int 196 inner error 197} 198 199func (e UnexpectedStatusCodeError) Error() string { 200 s := func(i int) string { return fmt.Sprintf("%d %s", i, http.StatusText(i)) } 201 202 got := s(e.got) 203 expected := []string{} 204 for _, v := range e.allowed { 205 expected = append(expected, s(v)) 206 } 207 return fmt.Sprintf("storage: status code from service response is %s; was expecting %s. Inner error: %+v", got, strings.Join(expected, " or "), e.inner) 208} 209 210// Got is the actual status code returned by Azure. 211func (e UnexpectedStatusCodeError) Got() int { 212 return e.got 213} 214 215// Inner returns any inner error info. 216func (e UnexpectedStatusCodeError) Inner() error { 217 return e.inner 218} 219 220// NewClientFromConnectionString creates a Client from the connection string. 221func NewClientFromConnectionString(input string) (Client, error) { 222 // build a map of connection string key/value pairs 223 parts := map[string]string{} 224 for _, pair := range strings.Split(input, ";") { 225 if pair == "" { 226 continue 227 } 228 229 equalDex := strings.IndexByte(pair, '=') 230 if equalDex <= 0 { 231 return Client{}, fmt.Errorf("Invalid connection segment %q", pair) 232 } 233 234 value := strings.TrimSpace(pair[equalDex+1:]) 235 key := strings.TrimSpace(strings.ToLower(pair[:equalDex])) 236 parts[key] = value 237 } 238 239 // TODO: validate parameter sets? 240 241 if parts[connectionStringAccountName] == StorageEmulatorAccountName { 242 return NewEmulatorClient() 243 } 244 245 if parts[connectionStringSAS] != "" { 246 endpoint := "" 247 if parts[connectionStringBlobEndpoint] != "" { 248 endpoint = parts[connectionStringBlobEndpoint] 249 } else if parts[connectionStringFileEndpoint] != "" { 250 endpoint = parts[connectionStringFileEndpoint] 251 } else if parts[connectionStringQueueEndpoint] != "" { 252 endpoint = parts[connectionStringQueueEndpoint] 253 } else { 254 endpoint = parts[connectionStringTableEndpoint] 255 } 256 257 return NewAccountSASClientFromEndpointToken(endpoint, parts[connectionStringSAS]) 258 } 259 260 useHTTPS := defaultUseHTTPS 261 if parts[connectionStringEndpointProtocol] != "" { 262 useHTTPS = parts[connectionStringEndpointProtocol] == "https" 263 } 264 265 return NewClient(parts[connectionStringAccountName], parts[connectionStringAccountKey], 266 parts[connectionStringEndpointSuffix], DefaultAPIVersion, useHTTPS) 267} 268 269// NewBasicClient constructs a Client with given storage service name and 270// key. 271func NewBasicClient(accountName, accountKey string) (Client, error) { 272 if accountName == StorageEmulatorAccountName { 273 return NewEmulatorClient() 274 } 275 return NewClient(accountName, accountKey, DefaultBaseURL, DefaultAPIVersion, defaultUseHTTPS) 276} 277 278// NewBasicClientOnSovereignCloud constructs a Client with given storage service name and 279// key in the referenced cloud. 280func NewBasicClientOnSovereignCloud(accountName, accountKey string, env azure.Environment) (Client, error) { 281 if accountName == StorageEmulatorAccountName { 282 return NewEmulatorClient() 283 } 284 return NewClient(accountName, accountKey, env.StorageEndpointSuffix, DefaultAPIVersion, defaultUseHTTPS) 285} 286 287//NewEmulatorClient contructs a Client intended to only work with Azure 288//Storage Emulator 289func NewEmulatorClient() (Client, error) { 290 return NewClient(StorageEmulatorAccountName, StorageEmulatorAccountKey, DefaultBaseURL, DefaultAPIVersion, false) 291} 292 293// NewClient constructs a Client. This should be used if the caller wants 294// to specify whether to use HTTPS, a specific REST API version or a custom 295// storage endpoint than Azure Public Cloud. 296func NewClient(accountName, accountKey, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) { 297 var c Client 298 if !IsValidStorageAccount(accountName) { 299 return c, fmt.Errorf("azure: account name is not valid: it must be between 3 and 24 characters, and only may contain numbers and lowercase letters: %v", accountName) 300 } else if accountKey == "" { 301 return c, fmt.Errorf("azure: account key required") 302 } else if serviceBaseURL == "" { 303 return c, fmt.Errorf("azure: base storage service url required") 304 } 305 306 key, err := base64.StdEncoding.DecodeString(accountKey) 307 if err != nil { 308 return c, fmt.Errorf("azure: malformed storage account key: %v", err) 309 } 310 311 c = Client{ 312 HTTPClient: http.DefaultClient, 313 accountName: accountName, 314 accountKey: key, 315 useHTTPS: useHTTPS, 316 baseURL: serviceBaseURL, 317 apiVersion: apiVersion, 318 sasClient: false, 319 UseSharedKeyLite: false, 320 Sender: &DefaultSender{ 321 RetryAttempts: defaultRetryAttempts, 322 ValidStatusCodes: defaultValidStatusCodes, 323 RetryDuration: defaultRetryDuration, 324 }, 325 } 326 c.userAgent = c.getDefaultUserAgent() 327 return c, nil 328} 329 330// IsValidStorageAccount checks if the storage account name is valid. 331// See https://docs.microsoft.com/en-us/azure/storage/storage-create-storage-account 332func IsValidStorageAccount(account string) bool { 333 return validStorageAccount.MatchString(account) 334} 335 336// NewAccountSASClient contructs a client that uses accountSAS authorization 337// for its operations. 338func NewAccountSASClient(account string, token url.Values, env azure.Environment) Client { 339 c := newSASClient() 340 c.accountSASToken = token 341 c.accountName = account 342 c.baseURL = env.StorageEndpointSuffix 343 344 // Get API version and protocol from token 345 c.apiVersion = token.Get("sv") 346 c.useHTTPS = token.Get("spr") == "https" 347 return c 348} 349 350// NewAccountSASClientFromEndpointToken constructs a client that uses accountSAS authorization 351// for its operations using the specified endpoint and SAS token. 352func NewAccountSASClientFromEndpointToken(endpoint string, sasToken string) (Client, error) { 353 u, err := url.Parse(endpoint) 354 if err != nil { 355 return Client{}, err 356 } 357 358 token, err := url.ParseQuery(sasToken) 359 if err != nil { 360 return Client{}, err 361 } 362 363 // the host name will look something like this 364 // - foo.blob.core.windows.net 365 // "foo" is the account name 366 // "core.windows.net" is the baseURL 367 368 // find the first dot to get account name 369 i1 := strings.IndexByte(u.Host, '.') 370 if i1 < 0 { 371 return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host) 372 } 373 374 // now find the second dot to get the base URL 375 i2 := strings.IndexByte(u.Host[i1+1:], '.') 376 if i2 < 0 { 377 return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host[i1+1:]) 378 } 379 380 c := newSASClient() 381 c.accountSASToken = token 382 c.accountName = u.Host[:i1] 383 c.baseURL = u.Host[i1+i2+2:] 384 385 // Get API version and protocol from token 386 c.apiVersion = token.Get("sv") 387 c.useHTTPS = token.Get("spr") == "https" 388 return c, nil 389} 390 391func newSASClient() Client { 392 c := Client{ 393 HTTPClient: http.DefaultClient, 394 apiVersion: DefaultAPIVersion, 395 sasClient: true, 396 Sender: &DefaultSender{ 397 RetryAttempts: defaultRetryAttempts, 398 ValidStatusCodes: defaultValidStatusCodes, 399 RetryDuration: defaultRetryDuration, 400 }, 401 } 402 c.userAgent = c.getDefaultUserAgent() 403 return c 404} 405 406func (c Client) isServiceSASClient() bool { 407 return c.sasClient && c.accountSASToken == nil 408} 409 410func (c Client) isAccountSASClient() bool { 411 return c.sasClient && c.accountSASToken != nil 412} 413 414func (c Client) getDefaultUserAgent() string { 415 return fmt.Sprintf("Go/%s (%s-%s) azure-storage-go/%s api-version/%s", 416 runtime.Version(), 417 runtime.GOARCH, 418 runtime.GOOS, 419 version.Number, 420 c.apiVersion, 421 ) 422} 423 424// AddToUserAgent adds an extension to the current user agent 425func (c *Client) AddToUserAgent(extension string) error { 426 if extension != "" { 427 c.userAgent = fmt.Sprintf("%s %s", c.userAgent, extension) 428 return nil 429 } 430 return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.userAgent) 431} 432 433// protectUserAgent is used in funcs that include extraheaders as a parameter. 434// It prevents the User-Agent header to be overwritten, instead if it happens to 435// be present, it gets added to the current User-Agent. Use it before getStandardHeaders 436func (c *Client) protectUserAgent(extraheaders map[string]string) map[string]string { 437 if v, ok := extraheaders[userAgentHeader]; ok { 438 c.AddToUserAgent(v) 439 delete(extraheaders, userAgentHeader) 440 } 441 return extraheaders 442} 443 444func (c Client) getBaseURL(service string) *url.URL { 445 scheme := "http" 446 if c.useHTTPS { 447 scheme = "https" 448 } 449 host := "" 450 if c.accountName == StorageEmulatorAccountName { 451 switch service { 452 case blobServiceName: 453 host = storageEmulatorBlob 454 case tableServiceName: 455 host = storageEmulatorTable 456 case queueServiceName: 457 host = storageEmulatorQueue 458 } 459 } else { 460 host = fmt.Sprintf("%s.%s.%s", c.accountName, service, c.baseURL) 461 } 462 463 return &url.URL{ 464 Scheme: scheme, 465 Host: host, 466 } 467} 468 469func (c Client) getEndpoint(service, path string, params url.Values) string { 470 u := c.getBaseURL(service) 471 472 // API doesn't accept path segments not starting with '/' 473 if !strings.HasPrefix(path, "/") { 474 path = fmt.Sprintf("/%v", path) 475 } 476 477 if c.accountName == StorageEmulatorAccountName { 478 path = fmt.Sprintf("/%v%v", StorageEmulatorAccountName, path) 479 } 480 481 u.Path = path 482 u.RawQuery = params.Encode() 483 return u.String() 484} 485 486// AccountSASTokenOptions includes options for constructing 487// an account SAS token. 488// https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas 489type AccountSASTokenOptions struct { 490 APIVersion string 491 Services Services 492 ResourceTypes ResourceTypes 493 Permissions Permissions 494 Start time.Time 495 Expiry time.Time 496 IP string 497 UseHTTPS bool 498} 499 500// Services specify services accessible with an account SAS. 501type Services struct { 502 Blob bool 503 Queue bool 504 Table bool 505 File bool 506} 507 508// ResourceTypes specify the resources accesible with an 509// account SAS. 510type ResourceTypes struct { 511 Service bool 512 Container bool 513 Object bool 514} 515 516// Permissions specifies permissions for an accountSAS. 517type Permissions struct { 518 Read bool 519 Write bool 520 Delete bool 521 List bool 522 Add bool 523 Create bool 524 Update bool 525 Process bool 526} 527 528// GetAccountSASToken creates an account SAS token 529// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas 530func (c Client) GetAccountSASToken(options AccountSASTokenOptions) (url.Values, error) { 531 if options.APIVersion == "" { 532 options.APIVersion = c.apiVersion 533 } 534 535 if options.APIVersion < "2015-04-05" { 536 return url.Values{}, fmt.Errorf("account SAS does not support API versions prior to 2015-04-05. API version : %s", options.APIVersion) 537 } 538 539 // build services string 540 services := "" 541 if options.Services.Blob { 542 services += "b" 543 } 544 if options.Services.Queue { 545 services += "q" 546 } 547 if options.Services.Table { 548 services += "t" 549 } 550 if options.Services.File { 551 services += "f" 552 } 553 554 // build resources string 555 resources := "" 556 if options.ResourceTypes.Service { 557 resources += "s" 558 } 559 if options.ResourceTypes.Container { 560 resources += "c" 561 } 562 if options.ResourceTypes.Object { 563 resources += "o" 564 } 565 566 // build permissions string 567 permissions := "" 568 if options.Permissions.Read { 569 permissions += "r" 570 } 571 if options.Permissions.Write { 572 permissions += "w" 573 } 574 if options.Permissions.Delete { 575 permissions += "d" 576 } 577 if options.Permissions.List { 578 permissions += "l" 579 } 580 if options.Permissions.Add { 581 permissions += "a" 582 } 583 if options.Permissions.Create { 584 permissions += "c" 585 } 586 if options.Permissions.Update { 587 permissions += "u" 588 } 589 if options.Permissions.Process { 590 permissions += "p" 591 } 592 593 // build start time, if exists 594 start := "" 595 if options.Start != (time.Time{}) { 596 start = options.Start.UTC().Format(time.RFC3339) 597 } 598 599 // build expiry time 600 expiry := options.Expiry.UTC().Format(time.RFC3339) 601 602 protocol := "https,http" 603 if options.UseHTTPS { 604 protocol = "https" 605 } 606 607 stringToSign := strings.Join([]string{ 608 c.accountName, 609 permissions, 610 services, 611 resources, 612 start, 613 expiry, 614 options.IP, 615 protocol, 616 options.APIVersion, 617 "", 618 }, "\n") 619 signature := c.computeHmac256(stringToSign) 620 621 sasParams := url.Values{ 622 "sv": {options.APIVersion}, 623 "ss": {services}, 624 "srt": {resources}, 625 "sp": {permissions}, 626 "se": {expiry}, 627 "spr": {protocol}, 628 "sig": {signature}, 629 } 630 if start != "" { 631 sasParams.Add("st", start) 632 } 633 if options.IP != "" { 634 sasParams.Add("sip", options.IP) 635 } 636 637 return sasParams, nil 638} 639 640// GetBlobService returns a BlobStorageClient which can operate on the blob 641// service of the storage account. 642func (c Client) GetBlobService() BlobStorageClient { 643 b := BlobStorageClient{ 644 client: c, 645 } 646 b.client.AddToUserAgent(blobServiceName) 647 b.auth = sharedKey 648 if c.UseSharedKeyLite { 649 b.auth = sharedKeyLite 650 } 651 return b 652} 653 654// GetQueueService returns a QueueServiceClient which can operate on the queue 655// service of the storage account. 656func (c Client) GetQueueService() QueueServiceClient { 657 q := QueueServiceClient{ 658 client: c, 659 } 660 q.client.AddToUserAgent(queueServiceName) 661 q.auth = sharedKey 662 if c.UseSharedKeyLite { 663 q.auth = sharedKeyLite 664 } 665 return q 666} 667 668// GetTableService returns a TableServiceClient which can operate on the table 669// service of the storage account. 670func (c Client) GetTableService() TableServiceClient { 671 t := TableServiceClient{ 672 client: c, 673 } 674 t.client.AddToUserAgent(tableServiceName) 675 t.auth = sharedKeyForTable 676 if c.UseSharedKeyLite { 677 t.auth = sharedKeyLiteForTable 678 } 679 return t 680} 681 682// GetFileService returns a FileServiceClient which can operate on the file 683// service of the storage account. 684func (c Client) GetFileService() FileServiceClient { 685 f := FileServiceClient{ 686 client: c, 687 } 688 f.client.AddToUserAgent(fileServiceName) 689 f.auth = sharedKey 690 if c.UseSharedKeyLite { 691 f.auth = sharedKeyLite 692 } 693 return f 694} 695 696func (c Client) getStandardHeaders() map[string]string { 697 return map[string]string{ 698 userAgentHeader: c.userAgent, 699 "x-ms-version": c.apiVersion, 700 "x-ms-date": currentTimeRfc1123Formatted(), 701 } 702} 703 704func (c Client) exec(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*http.Response, error) { 705 headers, err := c.addAuthorizationHeader(verb, url, headers, auth) 706 if err != nil { 707 return nil, err 708 } 709 710 req, err := http.NewRequest(verb, url, body) 711 if err != nil { 712 return nil, errors.New("azure/storage: error creating request: " + err.Error()) 713 } 714 715 // http.NewRequest() will automatically set req.ContentLength for a handful of types 716 // otherwise we will handle here. 717 if req.ContentLength < 1 { 718 if clstr, ok := headers["Content-Length"]; ok { 719 if cl, err := strconv.ParseInt(clstr, 10, 64); err == nil { 720 req.ContentLength = cl 721 } 722 } 723 } 724 725 for k, v := range headers { 726 req.Header[k] = append(req.Header[k], v) // Must bypass case munging present in `Add` by using map functions directly. See https://github.com/Azure/azure-sdk-for-go/issues/645 727 } 728 729 if c.isAccountSASClient() { 730 // append the SAS token to the query params 731 v := req.URL.Query() 732 v = mergeParams(v, c.accountSASToken) 733 req.URL.RawQuery = v.Encode() 734 } 735 736 resp, err := c.Sender.Send(&c, req) 737 if err != nil { 738 return nil, err 739 } 740 741 if resp.StatusCode >= 400 && resp.StatusCode <= 505 { 742 return resp, getErrorFromResponse(resp) 743 } 744 745 return resp, nil 746} 747 748func (c Client) execInternalJSONCommon(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, *http.Request, *http.Response, error) { 749 headers, err := c.addAuthorizationHeader(verb, url, headers, auth) 750 if err != nil { 751 return nil, nil, nil, err 752 } 753 754 req, err := http.NewRequest(verb, url, body) 755 for k, v := range headers { 756 req.Header.Add(k, v) 757 } 758 759 resp, err := c.Sender.Send(&c, req) 760 if err != nil { 761 return nil, nil, nil, err 762 } 763 764 respToRet := &odataResponse{resp: resp} 765 766 statusCode := resp.StatusCode 767 if statusCode >= 400 && statusCode <= 505 { 768 var respBody []byte 769 respBody, err = readAndCloseBody(resp.Body) 770 if err != nil { 771 return nil, nil, nil, err 772 } 773 774 requestID, date, version := getDebugHeaders(resp.Header) 775 if len(respBody) == 0 { 776 // no error in response body, might happen in HEAD requests 777 err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version) 778 return respToRet, req, resp, err 779 } 780 // try unmarshal as odata.error json 781 err = json.Unmarshal(respBody, &respToRet.odata) 782 } 783 784 return respToRet, req, resp, err 785} 786 787func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) { 788 respToRet, _, _, err := c.execInternalJSONCommon(verb, url, headers, body, auth) 789 return respToRet, err 790} 791 792func (c Client) execBatchOperationJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) { 793 // execute common query, get back generated request, response etc... for more processing. 794 respToRet, req, resp, err := c.execInternalJSONCommon(verb, url, headers, body, auth) 795 if err != nil { 796 return nil, err 797 } 798 799 // return the OData in the case of executing batch commands. 800 // In this case we need to read the outer batch boundary and contents. 801 // Then we read the changeset information within the batch 802 var respBody []byte 803 respBody, err = readAndCloseBody(resp.Body) 804 if err != nil { 805 return nil, err 806 } 807 808 // outer multipart body 809 _, batchHeader, err := mime.ParseMediaType(resp.Header["Content-Type"][0]) 810 if err != nil { 811 return nil, err 812 } 813 814 // batch details. 815 batchBoundary := batchHeader["boundary"] 816 batchPartBuf, changesetBoundary, err := genBatchReader(batchBoundary, respBody) 817 if err != nil { 818 return nil, err 819 } 820 821 // changeset details. 822 err = genChangesetReader(req, respToRet, batchPartBuf, changesetBoundary) 823 if err != nil { 824 return nil, err 825 } 826 827 return respToRet, nil 828} 829 830func genChangesetReader(req *http.Request, respToRet *odataResponse, batchPartBuf io.Reader, changesetBoundary string) error { 831 changesetMultiReader := multipart.NewReader(batchPartBuf, changesetBoundary) 832 changesetPart, err := changesetMultiReader.NextPart() 833 if err != nil { 834 return err 835 } 836 837 changesetPartBufioReader := bufio.NewReader(changesetPart) 838 changesetResp, err := http.ReadResponse(changesetPartBufioReader, req) 839 if err != nil { 840 return err 841 } 842 843 if changesetResp.StatusCode != http.StatusNoContent { 844 changesetBody, err := readAndCloseBody(changesetResp.Body) 845 err = json.Unmarshal(changesetBody, &respToRet.odata) 846 if err != nil { 847 return err 848 } 849 respToRet.resp = changesetResp 850 } 851 852 return nil 853} 854 855func genBatchReader(batchBoundary string, respBody []byte) (io.Reader, string, error) { 856 respBodyString := string(respBody) 857 respBodyReader := strings.NewReader(respBodyString) 858 859 // reading batchresponse 860 batchMultiReader := multipart.NewReader(respBodyReader, batchBoundary) 861 batchPart, err := batchMultiReader.NextPart() 862 if err != nil { 863 return nil, "", err 864 } 865 batchPartBufioReader := bufio.NewReader(batchPart) 866 867 _, changesetHeader, err := mime.ParseMediaType(batchPart.Header.Get("Content-Type")) 868 if err != nil { 869 return nil, "", err 870 } 871 changesetBoundary := changesetHeader["boundary"] 872 return batchPartBufioReader, changesetBoundary, nil 873} 874 875func readAndCloseBody(body io.ReadCloser) ([]byte, error) { 876 defer body.Close() 877 out, err := ioutil.ReadAll(body) 878 if err == io.EOF { 879 err = nil 880 } 881 return out, err 882} 883 884// reads the response body then closes it 885func drainRespBody(resp *http.Response) { 886 io.Copy(ioutil.Discard, resp.Body) 887 resp.Body.Close() 888} 889 890func serviceErrFromXML(body []byte, storageErr *AzureStorageServiceError) error { 891 if err := xml.Unmarshal(body, storageErr); err != nil { 892 storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body)) 893 return err 894 } 895 return nil 896} 897 898func serviceErrFromJSON(body []byte, storageErr *AzureStorageServiceError) error { 899 odataError := odataErrorWrapper{} 900 if err := json.Unmarshal(body, &odataError); err != nil { 901 storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body)) 902 return err 903 } 904 storageErr.Code = odataError.Err.Code 905 storageErr.Message = odataError.Err.Message.Value 906 storageErr.Lang = odataError.Err.Message.Lang 907 return nil 908} 909 910func serviceErrFromStatusCode(code int, status string, requestID, date, version string) AzureStorageServiceError { 911 return AzureStorageServiceError{ 912 StatusCode: code, 913 Code: status, 914 RequestID: requestID, 915 Date: date, 916 APIVersion: version, 917 Message: "no response body was available for error status code", 918 } 919} 920 921func (e AzureStorageServiceError) Error() string { 922 return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s, QueryParameterName=%s, QueryParameterValue=%s", 923 e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion, e.QueryParameterName, e.QueryParameterValue) 924} 925 926// checkRespCode returns UnexpectedStatusError if the given response code is not 927// one of the allowed status codes; otherwise nil. 928func checkRespCode(resp *http.Response, allowed []int) error { 929 for _, v := range allowed { 930 if resp.StatusCode == v { 931 return nil 932 } 933 } 934 err := getErrorFromResponse(resp) 935 return UnexpectedStatusCodeError{ 936 allowed: allowed, 937 got: resp.StatusCode, 938 inner: err, 939 } 940} 941 942func (c Client) addMetadataToHeaders(h map[string]string, metadata map[string]string) map[string]string { 943 metadata = c.protectUserAgent(metadata) 944 for k, v := range metadata { 945 h[userDefinedMetadataHeaderPrefix+k] = v 946 } 947 return h 948} 949 950func getDebugHeaders(h http.Header) (requestID, date, version string) { 951 requestID = h.Get("x-ms-request-id") 952 version = h.Get("x-ms-version") 953 date = h.Get("Date") 954 return 955} 956 957func getErrorFromResponse(resp *http.Response) error { 958 respBody, err := readAndCloseBody(resp.Body) 959 if err != nil { 960 return err 961 } 962 963 requestID, date, version := getDebugHeaders(resp.Header) 964 if len(respBody) == 0 { 965 // no error in response body, might happen in HEAD requests 966 err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version) 967 } else { 968 storageErr := AzureStorageServiceError{ 969 StatusCode: resp.StatusCode, 970 RequestID: requestID, 971 Date: date, 972 APIVersion: version, 973 } 974 // response contains storage service error object, unmarshal 975 if resp.Header.Get("Content-Type") == "application/xml" { 976 errIn := serviceErrFromXML(respBody, &storageErr) 977 if err != nil { // error unmarshaling the error response 978 err = errIn 979 } 980 } else { 981 errIn := serviceErrFromJSON(respBody, &storageErr) 982 if err != nil { // error unmarshaling the error response 983 err = errIn 984 } 985 } 986 err = storageErr 987 } 988 return err 989} 990