| // Package storage provides clients for Microsoft Azure Storage Services. |
| package storage |
| |
| // Copyright 2017 Microsoft Corporation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import ( |
| "bytes" |
| "fmt" |
| "net/url" |
| "sort" |
| "strings" |
| ) |
| |
| // See: https://docs.microsoft.com/rest/api/storageservices/fileservices/authentication-for-the-azure-storage-services |
| |
| type authentication string |
| |
| const ( |
| sharedKey authentication = "sharedKey" |
| sharedKeyForTable authentication = "sharedKeyTable" |
| sharedKeyLite authentication = "sharedKeyLite" |
| sharedKeyLiteForTable authentication = "sharedKeyLiteTable" |
| |
| // headers |
| headerAcceptCharset = "Accept-Charset" |
| headerAuthorization = "Authorization" |
| headerContentLength = "Content-Length" |
| headerDate = "Date" |
| headerXmsDate = "x-ms-date" |
| headerXmsVersion = "x-ms-version" |
| headerContentEncoding = "Content-Encoding" |
| headerContentLanguage = "Content-Language" |
| headerContentType = "Content-Type" |
| headerContentMD5 = "Content-MD5" |
| headerIfModifiedSince = "If-Modified-Since" |
| headerIfMatch = "If-Match" |
| headerIfNoneMatch = "If-None-Match" |
| headerIfUnmodifiedSince = "If-Unmodified-Since" |
| headerRange = "Range" |
| headerDataServiceVersion = "DataServiceVersion" |
| headerMaxDataServiceVersion = "MaxDataServiceVersion" |
| headerContentTransferEncoding = "Content-Transfer-Encoding" |
| ) |
| |
| func (c *Client) addAuthorizationHeader(verb, url string, headers map[string]string, auth authentication) (map[string]string, error) { |
| if !c.sasClient { |
| authHeader, err := c.getSharedKey(verb, url, headers, auth) |
| if err != nil { |
| return nil, err |
| } |
| headers[headerAuthorization] = authHeader |
| } |
| return headers, nil |
| } |
| |
| func (c *Client) getSharedKey(verb, url string, headers map[string]string, auth authentication) (string, error) { |
| canRes, err := c.buildCanonicalizedResource(url, auth, false) |
| if err != nil { |
| return "", err |
| } |
| |
| canString, err := buildCanonicalizedString(verb, headers, canRes, auth) |
| if err != nil { |
| return "", err |
| } |
| return c.createAuthorizationHeader(canString, auth), nil |
| } |
| |
| func (c *Client) buildCanonicalizedResource(uri string, auth authentication, sas bool) (string, error) { |
| errMsg := "buildCanonicalizedResource error: %s" |
| u, err := url.Parse(uri) |
| if err != nil { |
| return "", fmt.Errorf(errMsg, err.Error()) |
| } |
| |
| cr := bytes.NewBufferString("") |
| if c.accountName != StorageEmulatorAccountName || !sas { |
| cr.WriteString("/") |
| cr.WriteString(c.getCanonicalizedAccountName()) |
| } |
| |
| if len(u.Path) > 0 { |
| // 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.WriteString(u.EscapedPath()) |
| } |
| |
| params, err := url.ParseQuery(u.RawQuery) |
| if err != nil { |
| return "", fmt.Errorf(errMsg, err.Error()) |
| } |
| |
| // See https://github.com/Azure/azure-storage-net/blob/master/Lib/Common/Core/Util/AuthenticationUtility.cs#L277 |
| if auth == sharedKey { |
| if len(params) > 0 { |
| cr.WriteString("\n") |
| |
| keys := []string{} |
| for key := range params { |
| keys = append(keys, key) |
| } |
| sort.Strings(keys) |
| |
| completeParams := []string{} |
| for _, key := range keys { |
| if len(params[key]) > 1 { |
| sort.Strings(params[key]) |
| } |
| |
| completeParams = append(completeParams, fmt.Sprintf("%s:%s", key, strings.Join(params[key], ","))) |
| } |
| cr.WriteString(strings.Join(completeParams, "\n")) |
| } |
| } else { |
| // search for "comp" parameter, if exists then add it to canonicalizedresource |
| if v, ok := params["comp"]; ok { |
| cr.WriteString("?comp=" + v[0]) |
| } |
| } |
| |
| return string(cr.Bytes()), nil |
| } |
| |
| 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") |
| } |
| |
| func buildCanonicalizedString(verb string, headers map[string]string, canonicalizedResource string, auth authentication) (string, error) { |
| contentLength := headers[headerContentLength] |
| if contentLength == "0" { |
| contentLength = "" |
| } |
| date := headers[headerDate] |
| if v, ok := headers[headerXmsDate]; ok { |
| if auth == sharedKey || auth == sharedKeyLite { |
| date = "" |
| } else { |
| date = v |
| } |
| } |
| var canString string |
| switch auth { |
| case sharedKey: |
| canString = strings.Join([]string{ |
| verb, |
| headers[headerContentEncoding], |
| headers[headerContentLanguage], |
| contentLength, |
| headers[headerContentMD5], |
| headers[headerContentType], |
| date, |
| headers[headerIfModifiedSince], |
| headers[headerIfMatch], |
| headers[headerIfNoneMatch], |
| headers[headerIfUnmodifiedSince], |
| headers[headerRange], |
| buildCanonicalizedHeader(headers), |
| canonicalizedResource, |
| }, "\n") |
| case sharedKeyForTable: |
| canString = strings.Join([]string{ |
| verb, |
| headers[headerContentMD5], |
| headers[headerContentType], |
| date, |
| canonicalizedResource, |
| }, "\n") |
| case sharedKeyLite: |
| canString = strings.Join([]string{ |
| verb, |
| headers[headerContentMD5], |
| headers[headerContentType], |
| date, |
| buildCanonicalizedHeader(headers), |
| canonicalizedResource, |
| }, "\n") |
| case sharedKeyLiteForTable: |
| canString = strings.Join([]string{ |
| date, |
| canonicalizedResource, |
| }, "\n") |
| default: |
| return "", fmt.Errorf("%s authentication is not supported yet", auth) |
| } |
| return canString, nil |
| } |
| |
| func buildCanonicalizedHeader(headers map[string]string) string { |
| cm := make(map[string]string) |
| |
| for k, v := range headers { |
| headerName := strings.TrimSpace(strings.ToLower(k)) |
| if strings.HasPrefix(headerName, "x-ms-") { |
| cm[headerName] = v |
| } |
| } |
| |
| if len(cm) == 0 { |
| return "" |
| } |
| |
| keys := []string{} |
| for key := range cm { |
| keys = append(keys, key) |
| } |
| |
| sort.Strings(keys) |
| |
| ch := bytes.NewBufferString("") |
| |
| for _, key := range keys { |
| ch.WriteString(key) |
| ch.WriteRune(':') |
| ch.WriteString(cm[key]) |
| ch.WriteRune('\n') |
| } |
| |
| return strings.TrimSuffix(string(ch.Bytes()), "\n") |
| } |
| |
| func (c *Client) createAuthorizationHeader(canonicalizedString string, auth authentication) string { |
| signature := c.computeHmac256(canonicalizedString) |
| var key string |
| switch auth { |
| case sharedKey, sharedKeyForTable: |
| key = "SharedKey" |
| case sharedKeyLite, sharedKeyLiteForTable: |
| key = "SharedKeyLite" |
| } |
| return fmt.Sprintf("%s %s:%s", key, c.getCanonicalizedAccountName(), signature) |
| } |