|
Packit |
63bb0d |
package azblob
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
import (
|
|
Packit |
63bb0d |
"bytes"
|
|
Packit |
63bb0d |
"context"
|
|
Packit |
63bb0d |
"crypto/hmac"
|
|
Packit |
63bb0d |
"crypto/sha256"
|
|
Packit |
63bb0d |
"encoding/base64"
|
|
Packit |
63bb0d |
"errors"
|
|
Packit |
63bb0d |
"net/http"
|
|
Packit |
63bb0d |
"net/url"
|
|
Packit |
63bb0d |
"sort"
|
|
Packit |
63bb0d |
"strings"
|
|
Packit |
63bb0d |
"time"
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
"github.com/Azure/azure-pipeline-go/pipeline"
|
|
Packit |
63bb0d |
)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// NewSharedKeyCredential creates an immutable SharedKeyCredential containing the
|
|
Packit |
63bb0d |
// storage account's name and either its primary or secondary key.
|
|
Packit |
63bb0d |
func NewSharedKeyCredential(accountName, accountKey string) (*SharedKeyCredential, error) {
|
|
Packit |
63bb0d |
bytes, err := base64.StdEncoding.DecodeString(accountKey)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return &SharedKeyCredential{}, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return &SharedKeyCredential{accountName: accountName, accountKey: bytes}, nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// SharedKeyCredential contains an account's name and its primary or secondary key.
|
|
Packit |
63bb0d |
// It is immutable making it shareable and goroutine-safe.
|
|
Packit |
63bb0d |
type SharedKeyCredential struct {
|
|
Packit |
63bb0d |
// Only the NewSharedKeyCredential method should set these; all other methods should treat them as read-only
|
|
Packit |
63bb0d |
accountName string
|
|
Packit |
63bb0d |
accountKey []byte
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// AccountName returns the Storage account's name.
|
|
Packit |
63bb0d |
func (f SharedKeyCredential) AccountName() string {
|
|
Packit |
63bb0d |
return f.accountName
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func (f SharedKeyCredential) getAccountKey() []byte {
|
|
Packit |
63bb0d |
return f.accountKey
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// noop function to satisfy StorageAccountCredential interface
|
|
Packit |
63bb0d |
func (f SharedKeyCredential) getUDKParams() *UserDelegationKey {
|
|
Packit |
63bb0d |
return nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// New creates a credential policy object.
|
|
Packit |
63bb0d |
func (f *SharedKeyCredential) New(next pipeline.Policy, po *pipeline.PolicyOptions) pipeline.Policy {
|
|
Packit |
63bb0d |
return pipeline.PolicyFunc(func(ctx context.Context, request pipeline.Request) (pipeline.Response, error) {
|
|
Packit |
63bb0d |
// Add a x-ms-date header if it doesn't already exist
|
|
Packit |
63bb0d |
if d := request.Header.Get(headerXmsDate); d == "" {
|
|
Packit |
63bb0d |
request.Header[headerXmsDate] = []string{time.Now().UTC().Format(http.TimeFormat)}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
stringToSign, err := f.buildStringToSign(request)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
signature := f.ComputeHMACSHA256(stringToSign)
|
|
Packit |
63bb0d |
authHeader := strings.Join([]string{"SharedKey ", f.accountName, ":", signature}, "")
|
|
Packit |
63bb0d |
request.Header[headerAuthorization] = []string{authHeader}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
response, err := next.Do(ctx, request)
|
|
Packit |
63bb0d |
if err != nil && response != nil && response.Response() != nil && response.Response().StatusCode == http.StatusForbidden {
|
|
Packit |
63bb0d |
// Service failed to authenticate request, log it
|
|
Packit |
63bb0d |
po.Log(pipeline.LogError, "===== HTTP Forbidden status, String-to-Sign:\n"+stringToSign+"\n===============================\n")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return response, err
|
|
Packit |
63bb0d |
})
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// credentialMarker is a package-internal method that exists just to satisfy the Credential interface.
|
|
Packit |
63bb0d |
func (*SharedKeyCredential) credentialMarker() {}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Constants ensuring that header names are correctly spelled and consistently cased.
|
|
Packit |
63bb0d |
const (
|
|
Packit |
63bb0d |
headerAuthorization = "Authorization"
|
|
Packit |
63bb0d |
headerCacheControl = "Cache-Control"
|
|
Packit |
63bb0d |
headerContentEncoding = "Content-Encoding"
|
|
Packit |
63bb0d |
headerContentDisposition = "Content-Disposition"
|
|
Packit |
63bb0d |
headerContentLanguage = "Content-Language"
|
|
Packit |
63bb0d |
headerContentLength = "Content-Length"
|
|
Packit |
63bb0d |
headerContentMD5 = "Content-MD5"
|
|
Packit |
63bb0d |
headerContentType = "Content-Type"
|
|
Packit |
63bb0d |
headerDate = "Date"
|
|
Packit |
63bb0d |
headerIfMatch = "If-Match"
|
|
Packit |
63bb0d |
headerIfModifiedSince = "If-Modified-Since"
|
|
Packit |
63bb0d |
headerIfNoneMatch = "If-None-Match"
|
|
Packit |
63bb0d |
headerIfUnmodifiedSince = "If-Unmodified-Since"
|
|
Packit |
63bb0d |
headerRange = "Range"
|
|
Packit |
63bb0d |
headerUserAgent = "User-Agent"
|
|
Packit |
63bb0d |
headerXmsDate = "x-ms-date"
|
|
Packit |
63bb0d |
headerXmsVersion = "x-ms-version"
|
|
Packit |
63bb0d |
)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// ComputeHMACSHA256 generates a hash signature for an HTTP request or for a SAS.
|
|
Packit |
63bb0d |
func (f SharedKeyCredential) ComputeHMACSHA256(message string) (base64String string) {
|
|
Packit |
63bb0d |
h := hmac.New(sha256.New, f.accountKey)
|
|
Packit |
63bb0d |
h.Write([]byte(message))
|
|
Packit |
63bb0d |
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func (f *SharedKeyCredential) buildStringToSign(request pipeline.Request) (string, error) {
|
|
Packit |
63bb0d |
// https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services
|
|
Packit |
63bb0d |
headers := request.Header
|
|
Packit |
63bb0d |
contentLength := headers.Get(headerContentLength)
|
|
Packit |
63bb0d |
if contentLength == "0" {
|
|
Packit |
63bb0d |
contentLength = ""
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
canonicalizedResource, err := f.buildCanonicalizedResource(request.URL)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return "", err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
stringToSign := strings.Join([]string{
|
|
Packit |
63bb0d |
request.Method,
|
|
Packit |
63bb0d |
headers.Get(headerContentEncoding),
|
|
Packit |
63bb0d |
headers.Get(headerContentLanguage),
|
|
Packit |
63bb0d |
contentLength,
|
|
Packit |
63bb0d |
headers.Get(headerContentMD5),
|
|
Packit |
63bb0d |
headers.Get(headerContentType),
|
|
Packit |
63bb0d |
"", // Empty date because x-ms-date is expected (as per web page above)
|
|
Packit |
63bb0d |
headers.Get(headerIfModifiedSince),
|
|
Packit |
63bb0d |
headers.Get(headerIfMatch),
|
|
Packit |
63bb0d |
headers.Get(headerIfNoneMatch),
|
|
Packit |
63bb0d |
headers.Get(headerIfUnmodifiedSince),
|
|
Packit |
63bb0d |
headers.Get(headerRange),
|
|
Packit |
63bb0d |
buildCanonicalizedHeader(headers),
|
|
Packit |
63bb0d |
canonicalizedResource,
|
|
Packit |
63bb0d |
}, "\n")
|
|
Packit |
63bb0d |
return stringToSign, nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func buildCanonicalizedHeader(headers http.Header) string {
|
|
Packit |
63bb0d |
cm := map[string][]string{}
|
|
Packit |
63bb0d |
for k, v := range headers {
|
|
Packit |
63bb0d |
headerName := strings.TrimSpace(strings.ToLower(k))
|
|
Packit |
63bb0d |
if strings.HasPrefix(headerName, "x-ms-") {
|
|
Packit |
63bb0d |
cm[headerName] = v // NOTE: the value must not have any whitespace around it.
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if len(cm) == 0 {
|
|
Packit |
63bb0d |
return ""
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
keys := make([]string, 0, len(cm))
|
|
Packit |
63bb0d |
for key := range cm {
|
|
Packit |
63bb0d |
keys = append(keys, key)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
sort.Strings(keys)
|
|
Packit |
63bb0d |
ch := bytes.NewBufferString("")
|
|
Packit |
63bb0d |
for i, key := range keys {
|
|
Packit |
63bb0d |
if i > 0 {
|
|
Packit |
63bb0d |
ch.WriteRune('\n')
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
ch.WriteString(key)
|
|
Packit |
63bb0d |
ch.WriteRune(':')
|
|
Packit |
63bb0d |
ch.WriteString(strings.Join(cm[key], ","))
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return string(ch.Bytes())
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func (f *SharedKeyCredential) buildCanonicalizedResource(u *url.URL) (string, error) {
|
|
Packit |
63bb0d |
// https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services
|
|
Packit |
63bb0d |
cr := bytes.NewBufferString("/")
|
|
Packit |
63bb0d |
cr.WriteString(f.accountName)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if len(u.Path) > 0 {
|
|
Packit |
63bb0d |
// Any portion of the CanonicalizedResource string that is derived from
|
|
Packit |
63bb0d |
// the resource's URI should be encoded exactly as it is in the URI.
|
|
Packit |
63bb0d |
// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx
|
|
Packit |
63bb0d |
cr.WriteString(u.EscapedPath())
|
|
Packit |
63bb0d |
} else {
|
|
Packit |
63bb0d |
// a slash is required to indicate the root path
|
|
Packit |
63bb0d |
cr.WriteString("/")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// params is a map[string][]string; param name is key; params values is []string
|
|
Packit |
63bb0d |
params, err := url.ParseQuery(u.RawQuery) // Returns URL decoded values
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return "", errors.New("parsing query parameters must succeed, otherwise there might be serious problems in the SDK/generated code")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if len(params) > 0 { // There is at least 1 query parameter
|
|
Packit |
63bb0d |
paramNames := []string{} // We use this to sort the parameter key names
|
|
Packit |
63bb0d |
for paramName := range params {
|
|
Packit |
63bb0d |
paramNames = append(paramNames, paramName) // paramNames must be lowercase
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
sort.Strings(paramNames)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
for _, paramName := range paramNames {
|
|
Packit |
63bb0d |
paramValues := params[paramName]
|
|
Packit |
63bb0d |
sort.Strings(paramValues)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Join the sorted key values separated by ','
|
|
Packit |
63bb0d |
// Then prepend "keyName:"; then add this string to the buffer
|
|
Packit |
63bb0d |
cr.WriteString("\n" + paramName + ":" + strings.Join(paramValues, ","))
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return string(cr.Bytes()), nil
|
|
Packit |
63bb0d |
}
|