2015-06-11 22:30:18 +00:00
// Package storage provides clients for Microsoft Azure Storage Services.
2015-02-05 00:37:43 +00:00
package storage
import (
"bytes"
"encoding/base64"
2016-10-28 22:46:05 +00:00
"encoding/json"
2015-02-05 00:37:43 +00:00
"encoding/xml"
2016-02-08 22:29:21 +00:00
"errors"
2015-02-05 00:37:43 +00:00
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"sort"
2016-02-08 22:29:21 +00:00
"strconv"
2015-02-05 00:37:43 +00:00
"strings"
)
const (
2015-06-11 22:30:18 +00:00
// DefaultBaseURL is the domain name used for storage requests 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.
2016-02-08 22:29:21 +00:00
DefaultAPIVersion = "2015-02-21"
2015-06-11 22:30:18 +00:00
defaultUseHTTPS = true
2015-02-05 00:37:43 +00:00
2016-10-28 22:46:05 +00:00
// 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=="
2015-02-05 00:37:43 +00:00
blobServiceName = "blob"
tableServiceName = "table"
queueServiceName = "queue"
2016-02-08 22:29:21 +00:00
fileServiceName = "file"
2016-10-28 22:46:05 +00:00
storageEmulatorBlob = "127.0.0.1:10000"
storageEmulatorTable = "127.0.0.1:10002"
storageEmulatorQueue = "127.0.0.1:10001"
2015-02-05 00:37:43 +00:00
)
2015-06-11 22:30:18 +00:00
// Client is the object that needs to be constructed to perform
// operations on the storage account.
type Client struct {
2016-10-28 22:46:05 +00:00
// HTTPClient is the http.Client used to initiate API
// requests. If it is nil, http.DefaultClient is used.
HTTPClient * http . Client
2015-02-05 00:37:43 +00:00
accountName string
accountKey [ ] byte
2015-06-11 22:30:18 +00:00
useHTTPS bool
baseURL string
2015-02-05 00:37:43 +00:00
apiVersion string
}
type storageResponse struct {
statusCode int
headers http . Header
body io . ReadCloser
}
2016-10-28 22:46:05 +00:00
type odataResponse struct {
storageResponse
odata odataErrorMessage
}
2015-06-11 22:30:18 +00:00
// AzureStorageServiceError contains fields of the error response from
2015-02-05 00:37:43 +00:00
// Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx
// Some fields might be specific to certain calls.
2015-06-11 22:30:18 +00:00
type AzureStorageServiceError struct {
2015-02-05 00:37:43 +00:00
Code string ` xml:"Code" `
Message string ` xml:"Message" `
AuthenticationErrorDetail string ` xml:"AuthenticationErrorDetail" `
QueryParameterName string ` xml:"QueryParameterName" `
QueryParameterValue string ` xml:"QueryParameterValue" `
Reason string ` xml:"Reason" `
StatusCode int
2015-06-11 22:30:18 +00:00
RequestID string
}
2016-10-28 22:46:05 +00:00
type odataErrorMessageMessage struct {
Lang string ` json:"lang" `
Value string ` json:"value" `
}
type odataErrorMessageInternal struct {
Code string ` json:"code" `
Message odataErrorMessageMessage ` json:"message" `
}
type odataErrorMessage struct {
Err odataErrorMessageInternal ` json:"odata.error" `
}
2015-06-11 22:30:18 +00:00
// 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
}
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" , got , strings . Join ( expected , " or " ) )
2015-02-05 00:37:43 +00:00
}
2016-02-08 22:29:21 +00:00
// Got is the actual status code returned by Azure.
func ( e UnexpectedStatusCodeError ) Got ( ) int {
return e . got
}
2015-06-11 22:30:18 +00:00
// NewBasicClient constructs a Client with given storage service name and
// key.
func NewBasicClient ( accountName , accountKey string ) ( Client , error ) {
2016-10-28 22:46:05 +00:00
if accountName == StorageEmulatorAccountName {
return NewEmulatorClient ( )
}
2015-06-11 22:30:18 +00:00
return NewClient ( accountName , accountKey , DefaultBaseURL , DefaultAPIVersion , defaultUseHTTPS )
2016-12-13 05:12:39 +00:00
2015-02-05 00:37:43 +00:00
}
2016-10-28 22:46:05 +00:00
//NewEmulatorClient contructs a Client intended to only work with Azure
//Storage Emulator
func NewEmulatorClient ( ) ( Client , error ) {
return NewClient ( StorageEmulatorAccountName , StorageEmulatorAccountKey , DefaultBaseURL , DefaultAPIVersion , false )
}
2015-06-11 22:30:18 +00:00
// 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 , blobServiceBaseURL , apiVersion string , useHTTPS bool ) ( Client , error ) {
var c Client
2015-02-05 00:37:43 +00:00
if accountName == "" {
2015-03-24 04:57:24 +00:00
return c , fmt . Errorf ( "azure: account name required" )
2015-02-05 00:37:43 +00:00
} else if accountKey == "" {
2015-03-24 04:57:24 +00:00
return c , fmt . Errorf ( "azure: account key required" )
2015-06-11 22:30:18 +00:00
} else if blobServiceBaseURL == "" {
2015-03-24 04:57:24 +00:00
return c , fmt . Errorf ( "azure: base storage service url required" )
2015-02-05 00:37:43 +00:00
}
key , err := base64 . StdEncoding . DecodeString ( accountKey )
if err != nil {
2016-10-28 22:46:05 +00:00
return c , fmt . Errorf ( "azure: malformed storage account key: %v" , err )
2015-02-05 00:37:43 +00:00
}
2015-06-11 22:30:18 +00:00
return Client {
2015-02-05 00:37:43 +00:00
accountName : accountName ,
accountKey : key ,
2015-06-11 22:30:18 +00:00
useHTTPS : useHTTPS ,
baseURL : blobServiceBaseURL ,
2015-03-24 04:57:24 +00:00
apiVersion : apiVersion ,
} , nil
2015-02-05 00:37:43 +00:00
}
2015-06-11 22:30:18 +00:00
func ( c Client ) getBaseURL ( service string ) string {
2015-02-05 00:37:43 +00:00
scheme := "http"
2015-06-11 22:30:18 +00:00
if c . useHTTPS {
2015-02-05 00:37:43 +00:00
scheme = "https"
}
2016-10-28 22:46:05 +00:00
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 )
}
2015-02-05 00:37:43 +00:00
u := & url . URL {
Scheme : scheme ,
Host : host }
return u . String ( )
}
2015-06-11 22:30:18 +00:00
func ( c Client ) getEndpoint ( service , path string , params url . Values ) string {
u , err := url . Parse ( c . getBaseURL ( service ) )
2015-02-05 00:37:43 +00:00
if err != nil {
// really should not be happening
panic ( err )
}
2016-10-28 22:46:05 +00:00
// 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 )
2015-02-05 00:37:43 +00:00
}
u . Path = path
u . RawQuery = params . Encode ( )
return u . String ( )
}
2015-06-11 22:30:18 +00:00
// GetBlobService returns a BlobStorageClient which can operate on the blob
// service of the storage account.
func ( c Client ) GetBlobService ( ) BlobStorageClient {
return BlobStorageClient { c }
}
// GetQueueService returns a QueueServiceClient which can operate on the queue
// service of the storage account.
func ( c Client ) GetQueueService ( ) QueueServiceClient {
return QueueServiceClient { c }
2015-02-05 00:37:43 +00:00
}
2016-10-28 22:46:05 +00:00
// GetTableService returns a TableServiceClient which can operate on the table
// service of the storage account.
func ( c Client ) GetTableService ( ) TableServiceClient {
return TableServiceClient { c }
}
2016-02-08 22:29:21 +00:00
// GetFileService returns a FileServiceClient which can operate on the file
// service of the storage account.
func ( c Client ) GetFileService ( ) FileServiceClient {
return FileServiceClient { c }
}
2015-06-11 22:30:18 +00:00
func ( c Client ) createAuthorizationHeader ( canonicalizedString string ) string {
2015-02-05 00:37:43 +00:00
signature := c . computeHmac256 ( canonicalizedString )
2016-10-28 22:46:05 +00:00
return fmt . Sprintf ( "%s %s:%s" , "SharedKey" , c . getCanonicalizedAccountName ( ) , signature )
2015-02-05 00:37:43 +00:00
}
2015-06-11 22:30:18 +00:00
func ( c Client ) getAuthorizationHeader ( verb , url string , headers map [ string ] string ) ( string , error ) {
2015-02-05 00:37:43 +00:00
canonicalizedResource , err := c . buildCanonicalizedResource ( url )
if err != nil {
return "" , err
}
canonicalizedString := c . buildCanonicalizedString ( verb , headers , canonicalizedResource )
return c . createAuthorizationHeader ( canonicalizedString ) , nil
}
2015-06-11 22:30:18 +00:00
func ( c Client ) getStandardHeaders ( ) map [ string ] string {
2015-02-05 00:37:43 +00:00
return map [ string ] string {
"x-ms-version" : c . apiVersion ,
"x-ms-date" : currentTimeRfc1123Formatted ( ) ,
}
}
2016-10-28 22:46:05 +00:00
func ( c Client ) getCanonicalizedAccountName ( ) string {
// since we may be trying to access a secondary storage account, we need to
// remove the -secondary part of the storage name
return strings . TrimSuffix ( c . accountName , "-secondary" )
}
2015-06-11 22:30:18 +00:00
func ( c Client ) buildCanonicalizedHeader ( headers map [ string ] string ) string {
2015-02-05 00:37:43 +00:00
cm := make ( map [ string ] string )
for k , v := range headers {
headerName := strings . TrimSpace ( strings . ToLower ( k ) )
match , _ := regexp . MatchString ( "x-ms-" , headerName )
if match {
cm [ headerName ] = v
}
}
if len ( cm ) == 0 {
return ""
}
keys := make ( [ ] string , 0 , len ( cm ) )
for key := range cm {
keys = append ( keys , key )
}
sort . Strings ( keys )
ch := ""
for i , key := range keys {
if i == len ( keys ) - 1 {
ch += fmt . Sprintf ( "%s:%s" , key , cm [ key ] )
} else {
ch += fmt . Sprintf ( "%s:%s\n" , key , cm [ key ] )
}
}
return ch
}
2016-10-28 22:46:05 +00:00
func ( c Client ) buildCanonicalizedResourceTable ( uri string ) ( string , error ) {
errMsg := "buildCanonicalizedResourceTable error: %s"
u , err := url . Parse ( uri )
if err != nil {
return "" , fmt . Errorf ( errMsg , err . Error ( ) )
}
cr := "/" + c . getCanonicalizedAccountName ( )
if len ( u . Path ) > 0 {
2016-12-13 05:12:39 +00:00
cr += u . EscapedPath ( )
2016-10-28 22:46:05 +00:00
}
return cr , nil
}
2015-06-11 22:30:18 +00:00
func ( c Client ) buildCanonicalizedResource ( uri string ) ( string , error ) {
2015-02-05 00:37:43 +00:00
errMsg := "buildCanonicalizedResource error: %s"
u , err := url . Parse ( uri )
if err != nil {
return "" , fmt . Errorf ( errMsg , err . Error ( ) )
}
2016-10-28 22:46:05 +00:00
cr := "/" + c . getCanonicalizedAccountName ( )
2015-02-05 00:37:43 +00:00
if len ( u . Path ) > 0 {
2016-10-28 22:46:05 +00:00
// Any portion of the CanonicalizedResource string that is derived from
// the resource's URI should be encoded exactly as it is in the URI.
// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx
cr += u . EscapedPath ( )
2015-02-05 00:37:43 +00:00
}
params , err := url . ParseQuery ( u . RawQuery )
if err != nil {
return "" , fmt . Errorf ( errMsg , err . Error ( ) )
}
if len ( params ) > 0 {
cr += "\n"
keys := make ( [ ] string , 0 , len ( params ) )
for key := range params {
keys = append ( keys , key )
}
sort . Strings ( keys )
for i , key := range keys {
if len ( params [ key ] ) > 1 {
sort . Strings ( params [ key ] )
}
if i == len ( keys ) - 1 {
cr += fmt . Sprintf ( "%s:%s" , key , strings . Join ( params [ key ] , "," ) )
} else {
cr += fmt . Sprintf ( "%s:%s\n" , key , strings . Join ( params [ key ] , "," ) )
}
}
}
2016-10-28 22:46:05 +00:00
2015-02-05 00:37:43 +00:00
return cr , nil
}
2015-06-11 22:30:18 +00:00
func ( c Client ) buildCanonicalizedString ( verb string , headers map [ string ] string , canonicalizedResource string ) string {
2016-02-08 22:29:21 +00:00
contentLength := headers [ "Content-Length" ]
if contentLength == "0" {
contentLength = ""
}
2015-02-05 00:37:43 +00:00
canonicalizedString := fmt . Sprintf ( "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s" ,
verb ,
headers [ "Content-Encoding" ] ,
headers [ "Content-Language" ] ,
2016-02-08 22:29:21 +00:00
contentLength ,
2015-02-05 00:37:43 +00:00
headers [ "Content-MD5" ] ,
headers [ "Content-Type" ] ,
headers [ "Date" ] ,
2016-02-08 22:29:21 +00:00
headers [ "If-Modified-Since" ] ,
2015-02-05 00:37:43 +00:00
headers [ "If-Match" ] ,
headers [ "If-None-Match" ] ,
2016-02-08 22:29:21 +00:00
headers [ "If-Unmodified-Since" ] ,
2015-02-05 00:37:43 +00:00
headers [ "Range" ] ,
c . buildCanonicalizedHeader ( headers ) ,
canonicalizedResource )
return canonicalizedString
}
2015-06-11 22:30:18 +00:00
func ( c Client ) exec ( verb , url string , headers map [ string ] string , body io . Reader ) ( * storageResponse , error ) {
2015-02-05 00:37:43 +00:00
authHeader , err := c . getAuthorizationHeader ( verb , url , headers )
if err != nil {
return nil , err
}
headers [ "Authorization" ] = authHeader
if err != nil {
return nil , err
}
req , err := http . NewRequest ( verb , url , body )
2016-02-08 22:29:21 +00:00
if err != nil {
return nil , errors . New ( "azure/storage: error creating request: " + err . Error ( ) )
}
if clstr , ok := headers [ "Content-Length" ] ; ok {
// content length header is being signed, but completely ignored by golang.
// instead we have to use the ContentLength property on the request struct
// (see https://golang.org/src/net/http/request.go?s=18140:18370#L536 and
// https://golang.org/src/net/http/transfer.go?s=1739:2467#L49)
req . ContentLength , err = strconv . ParseInt ( clstr , 10 , 64 )
if err != nil {
return nil , err
}
}
2015-02-05 00:37:43 +00:00
for k , v := range headers {
req . Header . Add ( k , v )
}
2016-10-28 22:46:05 +00:00
httpClient := c . HTTPClient
if httpClient == nil {
httpClient = http . DefaultClient
}
2015-02-05 00:37:43 +00:00
resp , err := httpClient . Do ( req )
if err != nil {
return nil , err
}
statusCode := resp . StatusCode
2016-03-21 19:08:47 +00:00
if statusCode >= 400 && statusCode <= 505 {
2015-02-05 00:37:43 +00:00
var respBody [ ] byte
respBody , err = readResponseBody ( resp )
if err != nil {
return nil , err
}
2016-12-13 05:12:39 +00:00
requestID := resp . Header . Get ( "x-ms-request-id" )
2015-02-05 00:37:43 +00:00
if len ( respBody ) == 0 {
2016-12-13 05:12:39 +00:00
// no error in response body, might happen in HEAD requests
err = serviceErrFromStatusCode ( resp . StatusCode , resp . Status , requestID )
2015-02-05 00:37:43 +00:00
} else {
// response contains storage service error object, unmarshal
2016-12-13 05:12:39 +00:00
storageErr , errIn := serviceErrFromXML ( respBody , resp . StatusCode , requestID )
2015-02-05 00:37:43 +00:00
if err != nil { // error unmarshaling the error response
err = errIn
}
err = storageErr
}
return & storageResponse {
statusCode : resp . StatusCode ,
headers : resp . Header ,
body : ioutil . NopCloser ( bytes . NewReader ( respBody ) ) , /* restore the body */
} , err
}
return & storageResponse {
statusCode : resp . StatusCode ,
headers : resp . Header ,
body : resp . Body } , nil
}
2016-10-28 22:46:05 +00:00
func ( c Client ) execInternalJSON ( verb , url string , headers map [ string ] string , body io . Reader ) ( * odataResponse , error ) {
req , err := http . NewRequest ( verb , url , body )
for k , v := range headers {
req . Header . Add ( k , v )
}
httpClient := c . HTTPClient
if httpClient == nil {
httpClient = http . DefaultClient
}
resp , err := httpClient . Do ( req )
if err != nil {
return nil , err
}
respToRet := & odataResponse { }
respToRet . body = resp . Body
respToRet . statusCode = resp . StatusCode
respToRet . headers = resp . Header
statusCode := resp . StatusCode
if statusCode >= 400 && statusCode <= 505 {
var respBody [ ] byte
respBody , err = readResponseBody ( resp )
if err != nil {
return nil , err
}
if len ( respBody ) == 0 {
2016-12-13 05:12:39 +00:00
// no error in response body, might happen in HEAD requests
err = serviceErrFromStatusCode ( resp . StatusCode , resp . Status , resp . Header . Get ( "x-ms-request-id" ) )
2016-10-28 22:46:05 +00:00
return respToRet , err
}
// try unmarshal as odata.error json
err = json . Unmarshal ( respBody , & respToRet . odata )
return respToRet , err
}
return respToRet , nil
}
func ( c Client ) createSharedKeyLite ( url string , headers map [ string ] string ) ( string , error ) {
can , err := c . buildCanonicalizedResourceTable ( url )
if err != nil {
return "" , err
}
strToSign := headers [ "x-ms-date" ] + "\n" + can
hmac := c . computeHmac256 ( strToSign )
return fmt . Sprintf ( "SharedKeyLite %s:%s" , c . accountName , hmac ) , nil
}
func ( c Client ) execTable ( verb , url string , headers map [ string ] string , body io . Reader ) ( * odataResponse , error ) {
var err error
headers [ "Authorization" ] , err = c . createSharedKeyLite ( url , headers )
if err != nil {
return nil , err
}
return c . execInternalJSON ( verb , url , headers , body )
}
2015-02-05 00:37:43 +00:00
func readResponseBody ( resp * http . Response ) ( [ ] byte , error ) {
defer resp . Body . Close ( )
out , err := ioutil . ReadAll ( resp . Body )
if err == io . EOF {
err = nil
}
return out , err
}
2015-06-11 22:30:18 +00:00
func serviceErrFromXML ( body [ ] byte , statusCode int , requestID string ) ( AzureStorageServiceError , error ) {
var storageErr AzureStorageServiceError
2015-02-05 00:37:43 +00:00
if err := xml . Unmarshal ( body , & storageErr ) ; err != nil {
return storageErr , err
}
storageErr . StatusCode = statusCode
2015-06-11 22:30:18 +00:00
storageErr . RequestID = requestID
2015-02-05 00:37:43 +00:00
return storageErr , nil
}
2016-12-13 05:12:39 +00:00
func serviceErrFromStatusCode ( code int , status string , requestID string ) AzureStorageServiceError {
return AzureStorageServiceError {
StatusCode : code ,
Code : status ,
RequestID : requestID ,
Message : "no response body was available for error status code" ,
}
}
2015-06-11 22:30:18 +00:00
func ( e AzureStorageServiceError ) Error ( ) string {
2016-02-08 22:29:21 +00:00
return fmt . Sprintf ( "storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestId=%s, QueryParameterName=%s, QueryParameterValue=%s" ,
e . StatusCode , e . Code , e . Message , e . RequestID , e . QueryParameterName , e . QueryParameterValue )
2015-06-11 22:30:18 +00:00
}
// checkRespCode returns UnexpectedStatusError if the given response code is not
// one of the allowed status codes; otherwise nil.
func checkRespCode ( respCode int , allowed [ ] int ) error {
for _ , v := range allowed {
if respCode == v {
return nil
}
}
return UnexpectedStatusCodeError { allowed , respCode }
2015-02-05 00:37:43 +00:00
}