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"
"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
blobServiceName = "blob"
tableServiceName = "table"
queueServiceName = "queue"
2016-02-08 22:29:21 +00:00
fileServiceName = "file"
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 {
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
}
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
}
// 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 ) {
return NewClient ( accountName , accountKey , DefaultBaseURL , DefaultAPIVersion , defaultUseHTTPS )
2015-02-05 00:37:43 +00:00
}
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 {
2015-03-24 04:57:24 +00:00
return c , 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"
}
2015-06-11 22:30:18 +00:00
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 )
}
if path == "" {
path = "/" // API doesn't accept path segments not starting with '/'
}
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-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 )
return fmt . Sprintf ( "%s %s:%s" , "SharedKey" , c . accountName , signature )
}
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 ( ) ,
}
}
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
}
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 ( ) )
}
cr := "/" + c . accountName
if len ( u . Path ) > 0 {
cr += u . Path
}
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 ] , "," ) )
}
}
}
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 )
}
httpClient := http . Client { }
resp , err := httpClient . Do ( req )
if err != nil {
return nil , err
}
statusCode := resp . StatusCode
2016-02-08 22:29:21 +00:00
if statusCode >= 400 && statusCode <= 505 && statusCode != 404 {
2015-02-05 00:37:43 +00:00
var respBody [ ] byte
respBody , err = readResponseBody ( resp )
if err != nil {
return nil , err
}
if len ( respBody ) == 0 {
// no error in response body
2015-06-11 22:30:18 +00:00
err = fmt . Errorf ( "storage: service returned without a response body (%s)" , resp . Status )
2015-02-05 00:37:43 +00:00
} else {
// response contains storage service error object, unmarshal
2015-06-11 22:30:18 +00:00
storageErr , errIn := serviceErrFromXML ( respBody , resp . StatusCode , resp . Header . Get ( "x-ms-request-id" ) )
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
}
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
}
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
}