1// Package storage provides clients for Microsoft Azure Storage Services.
2package storage
3
4// Copyright 2017 Microsoft Corporation
5//
6//  Licensed under the Apache License, Version 2.0 (the "License");
7//  you may not use this file except in compliance with the License.
8//  You may obtain a copy of the License at
9//
10//      http://www.apache.org/licenses/LICENSE-2.0
11//
12//  Unless required by applicable law or agreed to in writing, software
13//  distributed under the License is distributed on an "AS IS" BASIS,
14//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//  See the License for the specific language governing permissions and
16//  limitations under the License.
17
18import (
19	"bytes"
20	"fmt"
21	"net/url"
22	"sort"
23	"strings"
24)
25
26// See: https://docs.microsoft.com/rest/api/storageservices/fileservices/authentication-for-the-azure-storage-services
27
28type authentication string
29
30const (
31	sharedKey             authentication = "sharedKey"
32	sharedKeyForTable     authentication = "sharedKeyTable"
33	sharedKeyLite         authentication = "sharedKeyLite"
34	sharedKeyLiteForTable authentication = "sharedKeyLiteTable"
35
36	// headers
37	headerAcceptCharset           = "Accept-Charset"
38	headerAuthorization           = "Authorization"
39	headerContentLength           = "Content-Length"
40	headerDate                    = "Date"
41	headerXmsDate                 = "x-ms-date"
42	headerXmsVersion              = "x-ms-version"
43	headerContentEncoding         = "Content-Encoding"
44	headerContentLanguage         = "Content-Language"
45	headerContentType             = "Content-Type"
46	headerContentMD5              = "Content-MD5"
47	headerIfModifiedSince         = "If-Modified-Since"
48	headerIfMatch                 = "If-Match"
49	headerIfNoneMatch             = "If-None-Match"
50	headerIfUnmodifiedSince       = "If-Unmodified-Since"
51	headerRange                   = "Range"
52	headerDataServiceVersion      = "DataServiceVersion"
53	headerMaxDataServiceVersion   = "MaxDataServiceVersion"
54	headerContentTransferEncoding = "Content-Transfer-Encoding"
55)
56
57func (c *Client) addAuthorizationHeader(verb, url string, headers map[string]string, auth authentication) (map[string]string, error) {
58	if !c.sasClient {
59		authHeader, err := c.getSharedKey(verb, url, headers, auth)
60		if err != nil {
61			return nil, err
62		}
63		headers[headerAuthorization] = authHeader
64	}
65	return headers, nil
66}
67
68func (c *Client) getSharedKey(verb, url string, headers map[string]string, auth authentication) (string, error) {
69	canRes, err := c.buildCanonicalizedResource(url, auth, false)
70	if err != nil {
71		return "", err
72	}
73
74	canString, err := buildCanonicalizedString(verb, headers, canRes, auth)
75	if err != nil {
76		return "", err
77	}
78	return c.createAuthorizationHeader(canString, auth), nil
79}
80
81func (c *Client) buildCanonicalizedResource(uri string, auth authentication, sas bool) (string, error) {
82	errMsg := "buildCanonicalizedResource error: %s"
83	u, err := url.Parse(uri)
84	if err != nil {
85		return "", fmt.Errorf(errMsg, err.Error())
86	}
87
88	cr := bytes.NewBufferString("")
89	if c.accountName != StorageEmulatorAccountName || !sas {
90		cr.WriteString("/")
91		cr.WriteString(c.getCanonicalizedAccountName())
92	}
93
94	if len(u.Path) > 0 {
95		// Any portion of the CanonicalizedResource string that is derived from
96		// the resource's URI should be encoded exactly as it is in the URI.
97		// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx
98		cr.WriteString(u.EscapedPath())
99	}
100
101	params, err := url.ParseQuery(u.RawQuery)
102	if err != nil {
103		return "", fmt.Errorf(errMsg, err.Error())
104	}
105
106	// See https://github.com/Azure/azure-storage-net/blob/master/Lib/Common/Core/Util/AuthenticationUtility.cs#L277
107	if auth == sharedKey {
108		if len(params) > 0 {
109			cr.WriteString("\n")
110
111			keys := []string{}
112			for key := range params {
113				keys = append(keys, key)
114			}
115			sort.Strings(keys)
116
117			completeParams := []string{}
118			for _, key := range keys {
119				if len(params[key]) > 1 {
120					sort.Strings(params[key])
121				}
122
123				completeParams = append(completeParams, fmt.Sprintf("%s:%s", key, strings.Join(params[key], ",")))
124			}
125			cr.WriteString(strings.Join(completeParams, "\n"))
126		}
127	} else {
128		// search for "comp" parameter, if exists then add it to canonicalizedresource
129		if v, ok := params["comp"]; ok {
130			cr.WriteString("?comp=" + v[0])
131		}
132	}
133
134	return string(cr.Bytes()), nil
135}
136
137func (c *Client) getCanonicalizedAccountName() string {
138	// since we may be trying to access a secondary storage account, we need to
139	// remove the -secondary part of the storage name
140	return strings.TrimSuffix(c.accountName, "-secondary")
141}
142
143func buildCanonicalizedString(verb string, headers map[string]string, canonicalizedResource string, auth authentication) (string, error) {
144	contentLength := headers[headerContentLength]
145	if contentLength == "0" {
146		contentLength = ""
147	}
148	date := headers[headerDate]
149	if v, ok := headers[headerXmsDate]; ok {
150		if auth == sharedKey || auth == sharedKeyLite {
151			date = ""
152		} else {
153			date = v
154		}
155	}
156	var canString string
157	switch auth {
158	case sharedKey:
159		canString = strings.Join([]string{
160			verb,
161			headers[headerContentEncoding],
162			headers[headerContentLanguage],
163			contentLength,
164			headers[headerContentMD5],
165			headers[headerContentType],
166			date,
167			headers[headerIfModifiedSince],
168			headers[headerIfMatch],
169			headers[headerIfNoneMatch],
170			headers[headerIfUnmodifiedSince],
171			headers[headerRange],
172			buildCanonicalizedHeader(headers),
173			canonicalizedResource,
174		}, "\n")
175	case sharedKeyForTable:
176		canString = strings.Join([]string{
177			verb,
178			headers[headerContentMD5],
179			headers[headerContentType],
180			date,
181			canonicalizedResource,
182		}, "\n")
183	case sharedKeyLite:
184		canString = strings.Join([]string{
185			verb,
186			headers[headerContentMD5],
187			headers[headerContentType],
188			date,
189			buildCanonicalizedHeader(headers),
190			canonicalizedResource,
191		}, "\n")
192	case sharedKeyLiteForTable:
193		canString = strings.Join([]string{
194			date,
195			canonicalizedResource,
196		}, "\n")
197	default:
198		return "", fmt.Errorf("%s authentication is not supported yet", auth)
199	}
200	return canString, nil
201}
202
203func buildCanonicalizedHeader(headers map[string]string) string {
204	cm := make(map[string]string)
205
206	for k, v := range headers {
207		headerName := strings.TrimSpace(strings.ToLower(k))
208		if strings.HasPrefix(headerName, "x-ms-") {
209			cm[headerName] = v
210		}
211	}
212
213	if len(cm) == 0 {
214		return ""
215	}
216
217	keys := []string{}
218	for key := range cm {
219		keys = append(keys, key)
220	}
221
222	sort.Strings(keys)
223
224	ch := bytes.NewBufferString("")
225
226	for _, key := range keys {
227		ch.WriteString(key)
228		ch.WriteRune(':')
229		ch.WriteString(cm[key])
230		ch.WriteRune('\n')
231	}
232
233	return strings.TrimSuffix(string(ch.Bytes()), "\n")
234}
235
236func (c *Client) createAuthorizationHeader(canonicalizedString string, auth authentication) string {
237	signature := c.computeHmac256(canonicalizedString)
238	var key string
239	switch auth {
240	case sharedKey, sharedKeyForTable:
241		key = "SharedKey"
242	case sharedKeyLite, sharedKeyLiteForTable:
243		key = "SharedKeyLite"
244	}
245	return fmt.Sprintf("%s %s:%s", key, c.getCanonicalizedAccountName(), signature)
246}
247