3e68d47da6
Signed-off-by: David Justice <david@devigned.com>
1061 lines
30 KiB
Go
1061 lines
30 KiB
Go
// Package storage provides clients for Microsoft Azure Storage Services.
|
|
package storage
|
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/version"
|
|
"github.com/Azure/go-autorest/autorest"
|
|
"github.com/Azure/go-autorest/autorest/azure"
|
|
)
|
|
|
|
const (
|
|
// DefaultBaseURL is the domain name used for storage requests in the
|
|
// public cloud when a default client is created.
|
|
DefaultBaseURL = "core.windows.net"
|
|
|
|
// DefaultAPIVersion is the Azure Storage API version string used when a
|
|
// basic client is created.
|
|
DefaultAPIVersion = "2018-03-28"
|
|
|
|
defaultUseHTTPS = true
|
|
defaultRetryAttempts = 5
|
|
defaultRetryDuration = time.Second * 5
|
|
|
|
// StorageEmulatorAccountName is the fixed storage account used by Azure Storage Emulator
|
|
StorageEmulatorAccountName = "devstoreaccount1"
|
|
|
|
// StorageEmulatorAccountKey is the the fixed storage account used by Azure Storage Emulator
|
|
StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
|
|
|
blobServiceName = "blob"
|
|
tableServiceName = "table"
|
|
queueServiceName = "queue"
|
|
fileServiceName = "file"
|
|
|
|
storageEmulatorBlob = "127.0.0.1:10000"
|
|
storageEmulatorTable = "127.0.0.1:10002"
|
|
storageEmulatorQueue = "127.0.0.1:10001"
|
|
|
|
userAgentHeader = "User-Agent"
|
|
|
|
userDefinedMetadataHeaderPrefix = "x-ms-meta-"
|
|
|
|
connectionStringAccountName = "accountname"
|
|
connectionStringAccountKey = "accountkey"
|
|
connectionStringEndpointSuffix = "endpointsuffix"
|
|
connectionStringEndpointProtocol = "defaultendpointsprotocol"
|
|
|
|
connectionStringBlobEndpoint = "blobendpoint"
|
|
connectionStringFileEndpoint = "fileendpoint"
|
|
connectionStringQueueEndpoint = "queueendpoint"
|
|
connectionStringTableEndpoint = "tableendpoint"
|
|
connectionStringSAS = "sharedaccesssignature"
|
|
)
|
|
|
|
var (
|
|
validStorageAccount = regexp.MustCompile("^[0-9a-z]{3,24}$")
|
|
validCosmosAccount = regexp.MustCompile("^[0-9a-z-]{3,44}$")
|
|
defaultValidStatusCodes = []int{
|
|
http.StatusRequestTimeout, // 408
|
|
http.StatusInternalServerError, // 500
|
|
http.StatusBadGateway, // 502
|
|
http.StatusServiceUnavailable, // 503
|
|
http.StatusGatewayTimeout, // 504
|
|
}
|
|
)
|
|
|
|
// Sender sends a request
|
|
type Sender interface {
|
|
Send(*Client, *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
// DefaultSender is the default sender for the client. It implements
|
|
// an automatic retry strategy.
|
|
type DefaultSender struct {
|
|
RetryAttempts int
|
|
RetryDuration time.Duration
|
|
ValidStatusCodes []int
|
|
attempts int // used for testing
|
|
}
|
|
|
|
// Send is the default retry strategy in the client
|
|
func (ds *DefaultSender) Send(c *Client, req *http.Request) (resp *http.Response, err error) {
|
|
rr := autorest.NewRetriableRequest(req)
|
|
for attempts := 0; attempts < ds.RetryAttempts; attempts++ {
|
|
err = rr.Prepare()
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
resp, err = c.HTTPClient.Do(rr.Request())
|
|
if err == nil && !autorest.ResponseHasStatusCode(resp, ds.ValidStatusCodes...) {
|
|
return resp, err
|
|
}
|
|
drainRespBody(resp)
|
|
autorest.DelayForBackoff(ds.RetryDuration, attempts, req.Cancel)
|
|
ds.attempts = attempts
|
|
}
|
|
ds.attempts++
|
|
return resp, err
|
|
}
|
|
|
|
// Client is the object that needs to be constructed to perform
|
|
// operations on the storage account.
|
|
type Client struct {
|
|
// HTTPClient is the http.Client used to initiate API
|
|
// requests. http.DefaultClient is used when creating a
|
|
// client.
|
|
HTTPClient *http.Client
|
|
|
|
// Sender is an interface that sends the request. Clients are
|
|
// created with a DefaultSender. The DefaultSender has an
|
|
// automatic retry strategy built in. The Sender can be customized.
|
|
Sender Sender
|
|
|
|
accountName string
|
|
accountKey []byte
|
|
useHTTPS bool
|
|
UseSharedKeyLite bool
|
|
baseURL string
|
|
apiVersion string
|
|
userAgent string
|
|
sasClient bool
|
|
accountSASToken url.Values
|
|
additionalHeaders map[string]string
|
|
}
|
|
|
|
type odataResponse struct {
|
|
resp *http.Response
|
|
odata odataErrorWrapper
|
|
}
|
|
|
|
// AzureStorageServiceError contains fields of the error response from
|
|
// Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx
|
|
// Some fields might be specific to certain calls.
|
|
type AzureStorageServiceError struct {
|
|
Code string `xml:"Code"`
|
|
Message string `xml:"Message"`
|
|
AuthenticationErrorDetail string `xml:"AuthenticationErrorDetail"`
|
|
QueryParameterName string `xml:"QueryParameterName"`
|
|
QueryParameterValue string `xml:"QueryParameterValue"`
|
|
Reason string `xml:"Reason"`
|
|
Lang string
|
|
StatusCode int
|
|
RequestID string
|
|
Date string
|
|
APIVersion string
|
|
}
|
|
|
|
// AzureTablesServiceError contains fields of the error response from
|
|
// Azure Table Storage Service REST API in Atom format.
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx
|
|
type AzureTablesServiceError struct {
|
|
Code string `xml:"code"`
|
|
Message string `xml:"message"`
|
|
StatusCode int
|
|
RequestID string
|
|
Date string
|
|
APIVersion string
|
|
}
|
|
|
|
func (e AzureTablesServiceError) Error() string {
|
|
return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s",
|
|
e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion)
|
|
}
|
|
|
|
type odataErrorMessage struct {
|
|
Lang string `json:"lang"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type odataError struct {
|
|
Code string `json:"code"`
|
|
Message odataErrorMessage `json:"message"`
|
|
}
|
|
|
|
type odataErrorWrapper struct {
|
|
Err odataError `json:"odata.error"`
|
|
}
|
|
|
|
// UnexpectedStatusCodeError is returned when a storage service responds with neither an error
|
|
// nor with an HTTP status code indicating success.
|
|
type UnexpectedStatusCodeError struct {
|
|
allowed []int
|
|
got int
|
|
inner error
|
|
}
|
|
|
|
func (e UnexpectedStatusCodeError) Error() string {
|
|
s := func(i int) string { return fmt.Sprintf("%d %s", i, http.StatusText(i)) }
|
|
|
|
got := s(e.got)
|
|
expected := []string{}
|
|
for _, v := range e.allowed {
|
|
expected = append(expected, s(v))
|
|
}
|
|
return fmt.Sprintf("storage: status code from service response is %s; was expecting %s. Inner error: %+v", got, strings.Join(expected, " or "), e.inner)
|
|
}
|
|
|
|
// Got is the actual status code returned by Azure.
|
|
func (e UnexpectedStatusCodeError) Got() int {
|
|
return e.got
|
|
}
|
|
|
|
// Inner returns any inner error info.
|
|
func (e UnexpectedStatusCodeError) Inner() error {
|
|
return e.inner
|
|
}
|
|
|
|
// NewClientFromConnectionString creates a Client from the connection string.
|
|
func NewClientFromConnectionString(input string) (Client, error) {
|
|
// build a map of connection string key/value pairs
|
|
parts := map[string]string{}
|
|
for _, pair := range strings.Split(input, ";") {
|
|
if pair == "" {
|
|
continue
|
|
}
|
|
|
|
equalDex := strings.IndexByte(pair, '=')
|
|
if equalDex <= 0 {
|
|
return Client{}, fmt.Errorf("Invalid connection segment %q", pair)
|
|
}
|
|
|
|
value := strings.TrimSpace(pair[equalDex+1:])
|
|
key := strings.TrimSpace(strings.ToLower(pair[:equalDex]))
|
|
parts[key] = value
|
|
}
|
|
|
|
// TODO: validate parameter sets?
|
|
|
|
if parts[connectionStringAccountName] == StorageEmulatorAccountName {
|
|
return NewEmulatorClient()
|
|
}
|
|
|
|
if parts[connectionStringSAS] != "" {
|
|
endpoint := ""
|
|
if parts[connectionStringBlobEndpoint] != "" {
|
|
endpoint = parts[connectionStringBlobEndpoint]
|
|
} else if parts[connectionStringFileEndpoint] != "" {
|
|
endpoint = parts[connectionStringFileEndpoint]
|
|
} else if parts[connectionStringQueueEndpoint] != "" {
|
|
endpoint = parts[connectionStringQueueEndpoint]
|
|
} else {
|
|
endpoint = parts[connectionStringTableEndpoint]
|
|
}
|
|
|
|
return NewAccountSASClientFromEndpointToken(endpoint, parts[connectionStringSAS])
|
|
}
|
|
|
|
useHTTPS := defaultUseHTTPS
|
|
if parts[connectionStringEndpointProtocol] != "" {
|
|
useHTTPS = parts[connectionStringEndpointProtocol] == "https"
|
|
}
|
|
|
|
return NewClient(parts[connectionStringAccountName], parts[connectionStringAccountKey],
|
|
parts[connectionStringEndpointSuffix], DefaultAPIVersion, useHTTPS)
|
|
}
|
|
|
|
// NewBasicClient constructs a Client with given storage service name and
|
|
// key.
|
|
func NewBasicClient(accountName, accountKey string) (Client, error) {
|
|
if accountName == StorageEmulatorAccountName {
|
|
return NewEmulatorClient()
|
|
}
|
|
return NewClient(accountName, accountKey, DefaultBaseURL, DefaultAPIVersion, defaultUseHTTPS)
|
|
}
|
|
|
|
// NewBasicClientOnSovereignCloud constructs a Client with given storage service name and
|
|
// key in the referenced cloud.
|
|
func NewBasicClientOnSovereignCloud(accountName, accountKey string, env azure.Environment) (Client, error) {
|
|
if accountName == StorageEmulatorAccountName {
|
|
return NewEmulatorClient()
|
|
}
|
|
return NewClient(accountName, accountKey, env.StorageEndpointSuffix, DefaultAPIVersion, defaultUseHTTPS)
|
|
}
|
|
|
|
//NewEmulatorClient contructs a Client intended to only work with Azure
|
|
//Storage Emulator
|
|
func NewEmulatorClient() (Client, error) {
|
|
return NewClient(StorageEmulatorAccountName, StorageEmulatorAccountKey, DefaultBaseURL, DefaultAPIVersion, false)
|
|
}
|
|
|
|
// NewClient constructs a Client. This should be used if the caller wants
|
|
// to specify whether to use HTTPS, a specific REST API version or a custom
|
|
// storage endpoint than Azure Public Cloud.
|
|
func NewClient(accountName, accountKey, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) {
|
|
var c Client
|
|
if !IsValidStorageAccount(accountName) {
|
|
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)
|
|
} else if accountKey == "" {
|
|
return c, fmt.Errorf("azure: account key required")
|
|
} else if serviceBaseURL == "" {
|
|
return c, fmt.Errorf("azure: base storage service url required")
|
|
}
|
|
|
|
key, err := base64.StdEncoding.DecodeString(accountKey)
|
|
if err != nil {
|
|
return c, fmt.Errorf("azure: malformed storage account key: %v", err)
|
|
}
|
|
|
|
return newClient(accountName, key, serviceBaseURL, apiVersion, useHTTPS)
|
|
}
|
|
|
|
// NewCosmosClient constructs a Client for Azure CosmosDB. This should be used if the caller wants
|
|
// to specify whether to use HTTPS, a specific REST API version or a custom
|
|
// cosmos endpoint than Azure Public Cloud.
|
|
func NewCosmosClient(accountName, accountKey, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) {
|
|
var c Client
|
|
if !IsValidCosmosAccount(accountName) {
|
|
return c, fmt.Errorf("azure: account name is not valid: The name can contain only lowercase letters, numbers and the '-' character, and must be between 3 and 44 characters: %v", accountName)
|
|
} else if accountKey == "" {
|
|
return c, fmt.Errorf("azure: account key required")
|
|
} else if serviceBaseURL == "" {
|
|
return c, fmt.Errorf("azure: base storage service url required")
|
|
}
|
|
|
|
key, err := base64.StdEncoding.DecodeString(accountKey)
|
|
if err != nil {
|
|
return c, fmt.Errorf("azure: malformed cosmos account key: %v", err)
|
|
}
|
|
|
|
return newClient(accountName, key, serviceBaseURL, apiVersion, useHTTPS)
|
|
}
|
|
|
|
// newClient constructs a Client with given parameters.
|
|
func newClient(accountName string, accountKey []byte, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) {
|
|
c := Client{
|
|
HTTPClient: http.DefaultClient,
|
|
accountName: accountName,
|
|
accountKey: accountKey,
|
|
useHTTPS: useHTTPS,
|
|
baseURL: serviceBaseURL,
|
|
apiVersion: apiVersion,
|
|
sasClient: false,
|
|
UseSharedKeyLite: false,
|
|
Sender: &DefaultSender{
|
|
RetryAttempts: defaultRetryAttempts,
|
|
ValidStatusCodes: defaultValidStatusCodes,
|
|
RetryDuration: defaultRetryDuration,
|
|
},
|
|
}
|
|
c.userAgent = c.getDefaultUserAgent()
|
|
return c, nil
|
|
}
|
|
|
|
// IsValidStorageAccount checks if the storage account name is valid.
|
|
// See https://docs.microsoft.com/en-us/azure/storage/storage-create-storage-account
|
|
func IsValidStorageAccount(account string) bool {
|
|
return validStorageAccount.MatchString(account)
|
|
}
|
|
|
|
// IsValidCosmosAccount checks if the Cosmos account name is valid.
|
|
// See https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-manage-database-account
|
|
func IsValidCosmosAccount(account string) bool {
|
|
return validCosmosAccount.MatchString(account)
|
|
}
|
|
|
|
// NewAccountSASClient contructs a client that uses accountSAS authorization
|
|
// for its operations.
|
|
func NewAccountSASClient(account string, token url.Values, env azure.Environment) Client {
|
|
return newSASClient(account, env.StorageEndpointSuffix, token)
|
|
}
|
|
|
|
// NewAccountSASClientFromEndpointToken constructs a client that uses accountSAS authorization
|
|
// for its operations using the specified endpoint and SAS token.
|
|
func NewAccountSASClientFromEndpointToken(endpoint string, sasToken string) (Client, error) {
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return Client{}, err
|
|
}
|
|
_, err = url.ParseQuery(sasToken)
|
|
if err != nil {
|
|
return Client{}, err
|
|
}
|
|
u.RawQuery = sasToken
|
|
return newSASClientFromURL(u)
|
|
}
|
|
|
|
func newSASClient(accountName, baseURL string, sasToken url.Values) Client {
|
|
c := Client{
|
|
HTTPClient: http.DefaultClient,
|
|
apiVersion: DefaultAPIVersion,
|
|
sasClient: true,
|
|
Sender: &DefaultSender{
|
|
RetryAttempts: defaultRetryAttempts,
|
|
ValidStatusCodes: defaultValidStatusCodes,
|
|
RetryDuration: defaultRetryDuration,
|
|
},
|
|
accountName: accountName,
|
|
baseURL: baseURL,
|
|
accountSASToken: sasToken,
|
|
useHTTPS: defaultUseHTTPS,
|
|
}
|
|
c.userAgent = c.getDefaultUserAgent()
|
|
// Get API version and protocol from token
|
|
c.apiVersion = sasToken.Get("sv")
|
|
if spr := sasToken.Get("spr"); spr != "" {
|
|
c.useHTTPS = spr == "https"
|
|
}
|
|
return c
|
|
}
|
|
|
|
func newSASClientFromURL(u *url.URL) (Client, error) {
|
|
// the host name will look something like this
|
|
// - foo.blob.core.windows.net
|
|
// "foo" is the account name
|
|
// "core.windows.net" is the baseURL
|
|
|
|
// find the first dot to get account name
|
|
i1 := strings.IndexByte(u.Host, '.')
|
|
if i1 < 0 {
|
|
return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host)
|
|
}
|
|
|
|
// now find the second dot to get the base URL
|
|
i2 := strings.IndexByte(u.Host[i1+1:], '.')
|
|
if i2 < 0 {
|
|
return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host[i1+1:])
|
|
}
|
|
|
|
sasToken := u.Query()
|
|
c := newSASClient(u.Host[:i1], u.Host[i1+i2+2:], sasToken)
|
|
if spr := sasToken.Get("spr"); spr == "" {
|
|
// infer from URL if not in the query params set
|
|
c.useHTTPS = u.Scheme == "https"
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c Client) isServiceSASClient() bool {
|
|
return c.sasClient && c.accountSASToken == nil
|
|
}
|
|
|
|
func (c Client) isAccountSASClient() bool {
|
|
return c.sasClient && c.accountSASToken != nil
|
|
}
|
|
|
|
func (c Client) getDefaultUserAgent() string {
|
|
return fmt.Sprintf("Go/%s (%s-%s) azure-storage-go/%s api-version/%s",
|
|
runtime.Version(),
|
|
runtime.GOARCH,
|
|
runtime.GOOS,
|
|
version.Number,
|
|
c.apiVersion,
|
|
)
|
|
}
|
|
|
|
// AddToUserAgent adds an extension to the current user agent
|
|
func (c *Client) AddToUserAgent(extension string) error {
|
|
if extension != "" {
|
|
c.userAgent = fmt.Sprintf("%s %s", c.userAgent, extension)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.userAgent)
|
|
}
|
|
|
|
// AddAdditionalHeaders adds additional standard headers
|
|
func (c *Client) AddAdditionalHeaders(headers map[string]string) {
|
|
if headers != nil {
|
|
c.additionalHeaders = map[string]string{}
|
|
for k, v := range headers {
|
|
c.additionalHeaders[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// protectUserAgent is used in funcs that include extraheaders as a parameter.
|
|
// It prevents the User-Agent header to be overwritten, instead if it happens to
|
|
// be present, it gets added to the current User-Agent. Use it before getStandardHeaders
|
|
func (c *Client) protectUserAgent(extraheaders map[string]string) map[string]string {
|
|
if v, ok := extraheaders[userAgentHeader]; ok {
|
|
c.AddToUserAgent(v)
|
|
delete(extraheaders, userAgentHeader)
|
|
}
|
|
return extraheaders
|
|
}
|
|
|
|
func (c Client) getBaseURL(service string) *url.URL {
|
|
scheme := "http"
|
|
if c.useHTTPS {
|
|
scheme = "https"
|
|
}
|
|
host := ""
|
|
if c.accountName == StorageEmulatorAccountName {
|
|
switch service {
|
|
case blobServiceName:
|
|
host = storageEmulatorBlob
|
|
case tableServiceName:
|
|
host = storageEmulatorTable
|
|
case queueServiceName:
|
|
host = storageEmulatorQueue
|
|
}
|
|
} else {
|
|
host = fmt.Sprintf("%s.%s.%s", c.accountName, service, c.baseURL)
|
|
}
|
|
|
|
return &url.URL{
|
|
Scheme: scheme,
|
|
Host: host,
|
|
}
|
|
}
|
|
|
|
func (c Client) getEndpoint(service, path string, params url.Values) string {
|
|
u := c.getBaseURL(service)
|
|
|
|
// API doesn't accept path segments not starting with '/'
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = fmt.Sprintf("/%v", path)
|
|
}
|
|
|
|
if c.accountName == StorageEmulatorAccountName {
|
|
path = fmt.Sprintf("/%v%v", StorageEmulatorAccountName, path)
|
|
}
|
|
|
|
u.Path = path
|
|
u.RawQuery = params.Encode()
|
|
return u.String()
|
|
}
|
|
|
|
// AccountSASTokenOptions includes options for constructing
|
|
// an account SAS token.
|
|
// https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas
|
|
type AccountSASTokenOptions struct {
|
|
APIVersion string
|
|
Services Services
|
|
ResourceTypes ResourceTypes
|
|
Permissions Permissions
|
|
Start time.Time
|
|
Expiry time.Time
|
|
IP string
|
|
UseHTTPS bool
|
|
}
|
|
|
|
// Services specify services accessible with an account SAS.
|
|
type Services struct {
|
|
Blob bool
|
|
Queue bool
|
|
Table bool
|
|
File bool
|
|
}
|
|
|
|
// ResourceTypes specify the resources accesible with an
|
|
// account SAS.
|
|
type ResourceTypes struct {
|
|
Service bool
|
|
Container bool
|
|
Object bool
|
|
}
|
|
|
|
// Permissions specifies permissions for an accountSAS.
|
|
type Permissions struct {
|
|
Read bool
|
|
Write bool
|
|
Delete bool
|
|
List bool
|
|
Add bool
|
|
Create bool
|
|
Update bool
|
|
Process bool
|
|
}
|
|
|
|
// GetAccountSASToken creates an account SAS token
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas
|
|
func (c Client) GetAccountSASToken(options AccountSASTokenOptions) (url.Values, error) {
|
|
if options.APIVersion == "" {
|
|
options.APIVersion = c.apiVersion
|
|
}
|
|
|
|
if options.APIVersion < "2015-04-05" {
|
|
return url.Values{}, fmt.Errorf("account SAS does not support API versions prior to 2015-04-05. API version : %s", options.APIVersion)
|
|
}
|
|
|
|
// build services string
|
|
services := ""
|
|
if options.Services.Blob {
|
|
services += "b"
|
|
}
|
|
if options.Services.Queue {
|
|
services += "q"
|
|
}
|
|
if options.Services.Table {
|
|
services += "t"
|
|
}
|
|
if options.Services.File {
|
|
services += "f"
|
|
}
|
|
|
|
// build resources string
|
|
resources := ""
|
|
if options.ResourceTypes.Service {
|
|
resources += "s"
|
|
}
|
|
if options.ResourceTypes.Container {
|
|
resources += "c"
|
|
}
|
|
if options.ResourceTypes.Object {
|
|
resources += "o"
|
|
}
|
|
|
|
// build permissions string
|
|
permissions := ""
|
|
if options.Permissions.Read {
|
|
permissions += "r"
|
|
}
|
|
if options.Permissions.Write {
|
|
permissions += "w"
|
|
}
|
|
if options.Permissions.Delete {
|
|
permissions += "d"
|
|
}
|
|
if options.Permissions.List {
|
|
permissions += "l"
|
|
}
|
|
if options.Permissions.Add {
|
|
permissions += "a"
|
|
}
|
|
if options.Permissions.Create {
|
|
permissions += "c"
|
|
}
|
|
if options.Permissions.Update {
|
|
permissions += "u"
|
|
}
|
|
if options.Permissions.Process {
|
|
permissions += "p"
|
|
}
|
|
|
|
// build start time, if exists
|
|
start := ""
|
|
if options.Start != (time.Time{}) {
|
|
start = options.Start.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
// build expiry time
|
|
expiry := options.Expiry.UTC().Format(time.RFC3339)
|
|
|
|
protocol := "https,http"
|
|
if options.UseHTTPS {
|
|
protocol = "https"
|
|
}
|
|
|
|
stringToSign := strings.Join([]string{
|
|
c.accountName,
|
|
permissions,
|
|
services,
|
|
resources,
|
|
start,
|
|
expiry,
|
|
options.IP,
|
|
protocol,
|
|
options.APIVersion,
|
|
"",
|
|
}, "\n")
|
|
signature := c.computeHmac256(stringToSign)
|
|
|
|
sasParams := url.Values{
|
|
"sv": {options.APIVersion},
|
|
"ss": {services},
|
|
"srt": {resources},
|
|
"sp": {permissions},
|
|
"se": {expiry},
|
|
"spr": {protocol},
|
|
"sig": {signature},
|
|
}
|
|
if start != "" {
|
|
sasParams.Add("st", start)
|
|
}
|
|
if options.IP != "" {
|
|
sasParams.Add("sip", options.IP)
|
|
}
|
|
|
|
return sasParams, nil
|
|
}
|
|
|
|
// GetBlobService returns a BlobStorageClient which can operate on the blob
|
|
// service of the storage account.
|
|
func (c Client) GetBlobService() BlobStorageClient {
|
|
b := BlobStorageClient{
|
|
client: c,
|
|
}
|
|
b.client.AddToUserAgent(blobServiceName)
|
|
b.auth = sharedKey
|
|
if c.UseSharedKeyLite {
|
|
b.auth = sharedKeyLite
|
|
}
|
|
return b
|
|
}
|
|
|
|
// GetQueueService returns a QueueServiceClient which can operate on the queue
|
|
// service of the storage account.
|
|
func (c Client) GetQueueService() QueueServiceClient {
|
|
q := QueueServiceClient{
|
|
client: c,
|
|
}
|
|
q.client.AddToUserAgent(queueServiceName)
|
|
q.auth = sharedKey
|
|
if c.UseSharedKeyLite {
|
|
q.auth = sharedKeyLite
|
|
}
|
|
return q
|
|
}
|
|
|
|
// GetTableService returns a TableServiceClient which can operate on the table
|
|
// service of the storage account.
|
|
func (c Client) GetTableService() TableServiceClient {
|
|
t := TableServiceClient{
|
|
client: c,
|
|
}
|
|
t.client.AddToUserAgent(tableServiceName)
|
|
t.auth = sharedKeyForTable
|
|
if c.UseSharedKeyLite {
|
|
t.auth = sharedKeyLiteForTable
|
|
}
|
|
return t
|
|
}
|
|
|
|
// GetFileService returns a FileServiceClient which can operate on the file
|
|
// service of the storage account.
|
|
func (c Client) GetFileService() FileServiceClient {
|
|
f := FileServiceClient{
|
|
client: c,
|
|
}
|
|
f.client.AddToUserAgent(fileServiceName)
|
|
f.auth = sharedKey
|
|
if c.UseSharedKeyLite {
|
|
f.auth = sharedKeyLite
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (c Client) getStandardHeaders() map[string]string {
|
|
headers := map[string]string{}
|
|
for k, v := range c.additionalHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
headers[userAgentHeader] = c.userAgent
|
|
headers["x-ms-version"] = c.apiVersion
|
|
headers["x-ms-date"] = currentTimeRfc1123Formatted()
|
|
|
|
return headers
|
|
}
|
|
|
|
func (c Client) exec(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*http.Response, error) {
|
|
headers, err := c.addAuthorizationHeader(verb, url, headers, auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest(verb, url, body)
|
|
if err != nil {
|
|
return nil, errors.New("azure/storage: error creating request: " + err.Error())
|
|
}
|
|
|
|
// http.NewRequest() will automatically set req.ContentLength for a handful of types
|
|
// otherwise we will handle here.
|
|
if req.ContentLength < 1 {
|
|
if clstr, ok := headers["Content-Length"]; ok {
|
|
if cl, err := strconv.ParseInt(clstr, 10, 64); err == nil {
|
|
req.ContentLength = cl
|
|
}
|
|
}
|
|
}
|
|
|
|
for k, v := range headers {
|
|
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
|
|
}
|
|
|
|
if c.isAccountSASClient() {
|
|
// append the SAS token to the query params
|
|
v := req.URL.Query()
|
|
v = mergeParams(v, c.accountSASToken)
|
|
req.URL.RawQuery = v.Encode()
|
|
}
|
|
|
|
resp, err := c.Sender.Send(&c, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode >= 400 && resp.StatusCode <= 505 {
|
|
return resp, getErrorFromResponse(resp)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (c Client) execInternalJSONCommon(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, *http.Request, *http.Response, error) {
|
|
headers, err := c.addAuthorizationHeader(verb, url, headers, auth)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest(verb, url, body)
|
|
for k, v := range headers {
|
|
req.Header.Add(k, v)
|
|
}
|
|
|
|
resp, err := c.Sender.Send(&c, req)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
respToRet := &odataResponse{resp: resp}
|
|
|
|
statusCode := resp.StatusCode
|
|
if statusCode >= 400 && statusCode <= 505 {
|
|
var respBody []byte
|
|
respBody, err = readAndCloseBody(resp.Body)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
requestID, date, version := getDebugHeaders(resp.Header)
|
|
if len(respBody) == 0 {
|
|
// no error in response body, might happen in HEAD requests
|
|
err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version)
|
|
return respToRet, req, resp, err
|
|
}
|
|
// response contains storage service error object, unmarshal
|
|
if resp.Header.Get("Content-Type") == "application/xml" {
|
|
storageErr := AzureTablesServiceError{
|
|
StatusCode: resp.StatusCode,
|
|
RequestID: requestID,
|
|
Date: date,
|
|
APIVersion: version,
|
|
}
|
|
if err := xml.Unmarshal(respBody, &storageErr); err != nil {
|
|
storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(respBody))
|
|
}
|
|
err = storageErr
|
|
} else {
|
|
err = json.Unmarshal(respBody, &respToRet.odata)
|
|
}
|
|
}
|
|
|
|
return respToRet, req, resp, err
|
|
}
|
|
|
|
func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) {
|
|
respToRet, _, _, err := c.execInternalJSONCommon(verb, url, headers, body, auth)
|
|
return respToRet, err
|
|
}
|
|
|
|
func (c Client) execBatchOperationJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) {
|
|
// execute common query, get back generated request, response etc... for more processing.
|
|
respToRet, req, resp, err := c.execInternalJSONCommon(verb, url, headers, body, auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// return the OData in the case of executing batch commands.
|
|
// In this case we need to read the outer batch boundary and contents.
|
|
// Then we read the changeset information within the batch
|
|
var respBody []byte
|
|
respBody, err = readAndCloseBody(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// outer multipart body
|
|
_, batchHeader, err := mime.ParseMediaType(resp.Header["Content-Type"][0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// batch details.
|
|
batchBoundary := batchHeader["boundary"]
|
|
batchPartBuf, changesetBoundary, err := genBatchReader(batchBoundary, respBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// changeset details.
|
|
err = genChangesetReader(req, respToRet, batchPartBuf, changesetBoundary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return respToRet, nil
|
|
}
|
|
|
|
func genChangesetReader(req *http.Request, respToRet *odataResponse, batchPartBuf io.Reader, changesetBoundary string) error {
|
|
changesetMultiReader := multipart.NewReader(batchPartBuf, changesetBoundary)
|
|
changesetPart, err := changesetMultiReader.NextPart()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
changesetPartBufioReader := bufio.NewReader(changesetPart)
|
|
changesetResp, err := http.ReadResponse(changesetPartBufioReader, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if changesetResp.StatusCode != http.StatusNoContent {
|
|
changesetBody, err := readAndCloseBody(changesetResp.Body)
|
|
err = json.Unmarshal(changesetBody, &respToRet.odata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
respToRet.resp = changesetResp
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func genBatchReader(batchBoundary string, respBody []byte) (io.Reader, string, error) {
|
|
respBodyString := string(respBody)
|
|
respBodyReader := strings.NewReader(respBodyString)
|
|
|
|
// reading batchresponse
|
|
batchMultiReader := multipart.NewReader(respBodyReader, batchBoundary)
|
|
batchPart, err := batchMultiReader.NextPart()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
batchPartBufioReader := bufio.NewReader(batchPart)
|
|
|
|
_, changesetHeader, err := mime.ParseMediaType(batchPart.Header.Get("Content-Type"))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
changesetBoundary := changesetHeader["boundary"]
|
|
return batchPartBufioReader, changesetBoundary, nil
|
|
}
|
|
|
|
func readAndCloseBody(body io.ReadCloser) ([]byte, error) {
|
|
defer body.Close()
|
|
out, err := ioutil.ReadAll(body)
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// reads the response body then closes it
|
|
func drainRespBody(resp *http.Response) {
|
|
if resp != nil {
|
|
io.Copy(ioutil.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func serviceErrFromXML(body []byte, storageErr *AzureStorageServiceError) error {
|
|
if err := xml.Unmarshal(body, storageErr); err != nil {
|
|
storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serviceErrFromJSON(body []byte, storageErr *AzureStorageServiceError) error {
|
|
odataError := odataErrorWrapper{}
|
|
if err := json.Unmarshal(body, &odataError); err != nil {
|
|
storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body))
|
|
return err
|
|
}
|
|
storageErr.Code = odataError.Err.Code
|
|
storageErr.Message = odataError.Err.Message.Value
|
|
storageErr.Lang = odataError.Err.Message.Lang
|
|
return nil
|
|
}
|
|
|
|
func serviceErrFromStatusCode(code int, status string, requestID, date, version string) AzureStorageServiceError {
|
|
return AzureStorageServiceError{
|
|
StatusCode: code,
|
|
Code: status,
|
|
RequestID: requestID,
|
|
Date: date,
|
|
APIVersion: version,
|
|
Message: "no response body was available for error status code",
|
|
}
|
|
}
|
|
|
|
func (e AzureStorageServiceError) Error() string {
|
|
return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s, QueryParameterName=%s, QueryParameterValue=%s",
|
|
e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion, e.QueryParameterName, e.QueryParameterValue)
|
|
}
|
|
|
|
// checkRespCode returns UnexpectedStatusError if the given response code is not
|
|
// one of the allowed status codes; otherwise nil.
|
|
func checkRespCode(resp *http.Response, allowed []int) error {
|
|
for _, v := range allowed {
|
|
if resp.StatusCode == v {
|
|
return nil
|
|
}
|
|
}
|
|
err := getErrorFromResponse(resp)
|
|
return UnexpectedStatusCodeError{
|
|
allowed: allowed,
|
|
got: resp.StatusCode,
|
|
inner: err,
|
|
}
|
|
}
|
|
|
|
func (c Client) addMetadataToHeaders(h map[string]string, metadata map[string]string) map[string]string {
|
|
metadata = c.protectUserAgent(metadata)
|
|
for k, v := range metadata {
|
|
h[userDefinedMetadataHeaderPrefix+k] = v
|
|
}
|
|
return h
|
|
}
|
|
|
|
func getDebugHeaders(h http.Header) (requestID, date, version string) {
|
|
requestID = h.Get("x-ms-request-id")
|
|
version = h.Get("x-ms-version")
|
|
date = h.Get("Date")
|
|
return
|
|
}
|
|
|
|
func getErrorFromResponse(resp *http.Response) error {
|
|
respBody, err := readAndCloseBody(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
requestID, date, version := getDebugHeaders(resp.Header)
|
|
if len(respBody) == 0 {
|
|
// no error in response body, might happen in HEAD requests
|
|
err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version)
|
|
} else {
|
|
storageErr := AzureStorageServiceError{
|
|
StatusCode: resp.StatusCode,
|
|
RequestID: requestID,
|
|
Date: date,
|
|
APIVersion: version,
|
|
}
|
|
// response contains storage service error object, unmarshal
|
|
if resp.Header.Get("Content-Type") == "application/xml" {
|
|
errIn := serviceErrFromXML(respBody, &storageErr)
|
|
if err != nil { // error unmarshaling the error response
|
|
err = errIn
|
|
}
|
|
} else {
|
|
errIn := serviceErrFromJSON(respBody, &storageErr)
|
|
if err != nil { // error unmarshaling the error response
|
|
err = errIn
|
|
}
|
|
}
|
|
err = storageErr
|
|
}
|
|
return err
|
|
}
|