| | 1 | | // Copyright (c) Microsoft Corporation. All rights reserved. |
| | 2 | | // Licensed under the MIT License. |
| | 3 | |
|
| | 4 | | using System; |
| | 5 | | using System.Globalization; |
| | 6 | | using System.Linq; |
| | 7 | | using System.Net.Http.Headers; |
| | 8 | | using System.Text; |
| | 9 | | using Azure.Core; |
| | 10 | | using Azure.Core.Pipeline; |
| | 11 | |
|
| | 12 | | namespace Azure.Storage |
| | 13 | | { |
| | 14 | | /// <summary> |
| | 15 | | /// HttpPipelinePolicy to sign requests using an Azure Storage shared key. |
| | 16 | | /// </summary> |
| | 17 | | internal sealed class StorageSharedKeyPipelinePolicy : HttpPipelineSynchronousPolicy |
| | 18 | | { |
| | 19 | | /// <summary> |
| | 20 | | /// Whether to always add the x-ms-date header. |
| | 21 | | /// </summary> |
| | 22 | | private const bool IncludeXMsDate = true; |
| | 23 | |
|
| | 24 | | /// <summary> |
| | 25 | | /// Shared key credentials used to sign requests |
| | 26 | | /// </summary> |
| | 27 | | private readonly StorageSharedKeyCredential _credentials; |
| | 28 | |
|
| | 29 | | /// <summary> |
| | 30 | | /// Create a new SharedKeyPipelinePolicy |
| | 31 | | /// </summary> |
| | 32 | | /// <param name="credentials">SharedKeyCredentials to authenticate requests.</param> |
| 272 | 33 | | public StorageSharedKeyPipelinePolicy(StorageSharedKeyCredential credentials) |
| 272 | 34 | | => _credentials = credentials; |
| | 35 | |
|
| | 36 | | /// <summary> |
| | 37 | | /// Sign the request using the shared key credentials. |
| | 38 | | /// </summary> |
| | 39 | | /// <param name="message">The message with the request to sign.</param> |
| | 40 | | public override void OnSendingRequest(HttpMessage message) |
| | 41 | | { |
| 726 | 42 | | base.OnSendingRequest(message); |
| | 43 | |
|
| | 44 | | // Add a x-ms-date header |
| | 45 | | if (IncludeXMsDate) |
| | 46 | | { |
| 726 | 47 | | var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture); |
| 726 | 48 | | message.Request.Headers.SetValue(Constants.HeaderNames.Date, date); |
| | 49 | | } |
| | 50 | |
|
| 726 | 51 | | var stringToSign = BuildStringToSign(message); |
| 726 | 52 | | var signature = StorageSharedKeyCredentialInternals.ComputeSasSignature(_credentials, stringToSign); |
| | 53 | |
|
| 726 | 54 | | var key = new AuthenticationHeaderValue(Constants.HeaderNames.SharedKey, _credentials.AccountName + ":" + si |
| 726 | 55 | | message.Request.Headers.SetValue(Constants.HeaderNames.Authorization, key); |
| 726 | 56 | | } |
| | 57 | |
|
| | 58 | | private string BuildStringToSign(HttpMessage message) |
| | 59 | | { |
| | 60 | | // https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key |
| | 61 | |
|
| 726 | 62 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentEncoding, out var contentEncoding); |
| 726 | 63 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentLanguage, out var contentLanguage); |
| 726 | 64 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentLength, out var contentLength); |
| 726 | 65 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentMD5, out var contentMD5); |
| 726 | 66 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentType, out var contentType); |
| 726 | 67 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.IfModifiedSince, out var ifModifiedSince); |
| 726 | 68 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.IfMatch, out var ifMatch); |
| 726 | 69 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.IfNoneMatch, out var ifNoneMatch); |
| 726 | 70 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.IfUnmodifiedSince, out var ifUnmodifiedSince); |
| 726 | 71 | | message.Request.Headers.TryGetValue(Constants.HeaderNames.Range, out var range); |
| | 72 | |
|
| 726 | 73 | | var stringToSign = string.Join("\n", |
| 726 | 74 | | message.Request.Method.ToString().ToUpperInvariant(), |
| 726 | 75 | | contentEncoding ?? "", |
| 726 | 76 | | contentLanguage ?? "", |
| 726 | 77 | | contentLength == "0" ? "" : contentLength ?? "", |
| 726 | 78 | | contentMD5 ?? "", // todo: fix base 64 VALUE |
| 726 | 79 | | contentType ?? "", |
| 726 | 80 | | "", // Empty date because x-ms-date is expected (as per web page above) |
| 726 | 81 | | ifModifiedSince ?? "", |
| 726 | 82 | | ifMatch ?? "", |
| 726 | 83 | | ifNoneMatch ?? "", |
| 726 | 84 | | ifUnmodifiedSince ?? "", |
| 726 | 85 | | range ?? "", |
| 726 | 86 | | BuildCanonicalizedHeaders(message), |
| 726 | 87 | | BuildCanonicalizedResource(message.Request.Uri.ToUri())); |
| 726 | 88 | | return stringToSign; |
| | 89 | | } |
| | 90 | |
|
| | 91 | | private static string BuildCanonicalizedHeaders(HttpMessage message) |
| | 92 | | { |
| | 93 | | // Grab all the "x-ms-*" headers, trim whitespace, lowercase, sort, |
| | 94 | | // and combine them with their values (separated by a colon). |
| 726 | 95 | | var sb = new StringBuilder(); |
| 6702 | 96 | | foreach (var headerName in |
| 726 | 97 | | message.Request.Headers |
| 4752 | 98 | | .Select(h => h.Name.ToLowerInvariant()) |
| 4752 | 99 | | .Where(name => name.StartsWith(Constants.HeaderNames.XMsPrefix, StringComparison.OrdinalIgnoreCase)) |
| 726 | 100 | | #pragma warning disable CA1308 // Normalize strings to uppercase |
| 3714 | 101 | | .OrderBy(name => name.Trim())) |
| | 102 | | #pragma warning restore CA1308 // Normalize strings to uppercase |
| | 103 | | { |
| 2988 | 104 | | if (sb.Length > 0) |
| | 105 | | { |
| 2262 | 106 | | sb.Append('\n'); |
| | 107 | | } |
| 2988 | 108 | | message.Request.Headers.TryGetValue(headerName, out var value); |
| 2988 | 109 | | sb.Append(headerName).Append(':').Append(value); |
| | 110 | | } |
| 726 | 111 | | return sb.ToString(); |
| | 112 | | } |
| | 113 | |
|
| | 114 | | private string BuildCanonicalizedResource(Uri resource) |
| | 115 | | { |
| | 116 | | // https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services |
| 726 | 117 | | StringBuilder cr = new StringBuilder("/").Append(_credentials.AccountName); |
| 726 | 118 | | if (resource.AbsolutePath.Length > 0) |
| | 119 | | { |
| | 120 | | // Any portion of the CanonicalizedResource string that is derived from |
| | 121 | | // the resource's URI should be encoded exactly as it is in the URI. |
| | 122 | | // -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx |
| 726 | 123 | | cr.Append(resource.AbsolutePath);//EscapedPath() |
| | 124 | | } |
| | 125 | | else |
| | 126 | | { |
| | 127 | | // a slash is required to indicate the root path |
| 0 | 128 | | cr.Append('/'); |
| | 129 | | } |
| | 130 | |
|
| 726 | 131 | | System.Collections.Generic.IDictionary<string, string> parameters = resource.GetQueryParameters(); // Return |
| 726 | 132 | | if (parameters.Count > 0) |
| | 133 | | { |
| 1234 | 134 | | foreach (var name in parameters.Keys.OrderBy(key => key, StringComparer.Ordinal)) |
| | 135 | | { |
| | 136 | | #pragma warning disable CA1308 // Normalize strings to uppercase |
| 266 | 137 | | cr.Append('\n').Append(name.ToLowerInvariant()).Append(':').Append(parameters[name]); |
| | 138 | | #pragma warning restore CA1308 // Normalize strings to uppercase |
| | 139 | | } |
| | 140 | | } |
| 726 | 141 | | return cr.ToString(); |
| | 142 | | } |
| | 143 | | } |
| | 144 | | } |