|  |  | 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> | 
|  | 539 | 33 |  |         public StorageSharedKeyPipelinePolicy(StorageSharedKeyCredential credentials) | 
|  | 539 | 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 |  |         { | 
|  | 3292 | 42 |  |             base.OnSendingRequest(message); | 
|  |  | 43 |  |  | 
|  |  | 44 |  |             // Add a x-ms-date header | 
|  |  | 45 |  |             if (IncludeXMsDate) | 
|  |  | 46 |  |             { | 
|  | 3292 | 47 |  |                 var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture); | 
|  | 3292 | 48 |  |                 message.Request.Headers.SetValue(Constants.HeaderNames.Date, date); | 
|  |  | 49 |  |             } | 
|  |  | 50 |  |  | 
|  | 3292 | 51 |  |             var stringToSign = BuildStringToSign(message); | 
|  | 3292 | 52 |  |             var signature = StorageSharedKeyCredentialInternals.ComputeSasSignature(_credentials, stringToSign); | 
|  |  | 53 |  |  | 
|  | 3292 | 54 |  |             var key = new AuthenticationHeaderValue(Constants.HeaderNames.SharedKey, _credentials.AccountName + ":" + si | 
|  | 3292 | 55 |  |             message.Request.Headers.SetValue(Constants.HeaderNames.Authorization, key); | 
|  | 3292 | 56 |  |         } | 
|  |  | 57 |  |  | 
|  |  | 58 |  |         private string BuildStringToSign(HttpMessage message) | 
|  |  | 59 |  |         { | 
|  |  | 60 |  |             // https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key | 
|  |  | 61 |  |  | 
|  | 3292 | 62 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentEncoding, out var contentEncoding); | 
|  | 3292 | 63 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentLanguage, out var contentLanguage); | 
|  | 3292 | 64 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentLength, out var contentLength); | 
|  | 3292 | 65 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentMD5, out var contentMD5); | 
|  | 3292 | 66 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.ContentType, out var contentType); | 
|  | 3292 | 67 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.IfModifiedSince, out var ifModifiedSince); | 
|  | 3292 | 68 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.IfMatch, out var ifMatch); | 
|  | 3292 | 69 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.IfNoneMatch, out var ifNoneMatch); | 
|  | 3292 | 70 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.IfUnmodifiedSince, out var ifUnmodifiedSince); | 
|  | 3292 | 71 |  |             message.Request.Headers.TryGetValue(Constants.HeaderNames.Range, out var range); | 
|  |  | 72 |  |  | 
|  | 3292 | 73 |  |             var stringToSign = string.Join("\n", | 
|  | 3292 | 74 |  |                 message.Request.Method.ToString().ToUpperInvariant(), | 
|  | 3292 | 75 |  |                 contentEncoding ?? "", | 
|  | 3292 | 76 |  |                 contentLanguage ?? "", | 
|  | 3292 | 77 |  |                 contentLength == "0" ? "" : contentLength ?? "", | 
|  | 3292 | 78 |  |                 contentMD5 ?? "", // todo: fix base 64 VALUE | 
|  | 3292 | 79 |  |                 contentType ?? "", | 
|  | 3292 | 80 |  |                 "", // Empty date because x-ms-date is expected (as per web page above) | 
|  | 3292 | 81 |  |                 ifModifiedSince ?? "", | 
|  | 3292 | 82 |  |                 ifMatch ?? "", | 
|  | 3292 | 83 |  |                 ifNoneMatch ?? "", | 
|  | 3292 | 84 |  |                 ifUnmodifiedSince ?? "", | 
|  | 3292 | 85 |  |                 range ?? "", | 
|  | 3292 | 86 |  |                 BuildCanonicalizedHeaders(message), | 
|  | 3292 | 87 |  |                 BuildCanonicalizedResource(message.Request.Uri.ToUri())); | 
|  | 3292 | 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). | 
|  | 3292 | 95 |  |             var sb = new StringBuilder(); | 
|  | 42440 | 96 |  |             foreach (var headerName in | 
|  | 3292 | 97 |  |                 message.Request.Headers | 
|  | 27528 | 98 |  |                 .Select(h => h.Name.ToLowerInvariant()) | 
|  | 27528 | 99 |  |                 .Where(name => name.StartsWith(Constants.HeaderNames.XMsPrefix, StringComparison.OrdinalIgnoreCase)) | 
|  | 3292 | 100 |  | #pragma warning disable CA1308 // Normalize strings to uppercase | 
|  | 22866 | 101 |  |                 .OrderBy(name => name.Trim())) | 
|  |  | 102 |  | #pragma warning restore CA1308 // Normalize strings to uppercase | 
|  |  | 103 |  |             { | 
|  | 19574 | 104 |  |                 if (sb.Length > 0) | 
|  |  | 105 |  |                 { | 
|  | 16282 | 106 |  |                     sb.Append('\n'); | 
|  |  | 107 |  |                 } | 
|  | 19574 | 108 |  |                 message.Request.Headers.TryGetValue(headerName, out var value); | 
|  | 19574 | 109 |  |                 sb.Append(headerName).Append(':').Append(value); | 
|  |  | 110 |  |             } | 
|  | 3292 | 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 | 
|  | 3292 | 117 |  |             StringBuilder cr = new StringBuilder("/").Append(_credentials.AccountName); | 
|  | 3292 | 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 | 
|  | 3292 | 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 |  |  | 
|  | 3292 | 131 |  |             System.Collections.Generic.IDictionary<string, string> parameters = resource.GetQueryParameters(); // Return | 
|  | 3292 | 132 |  |             if (parameters.Count > 0) | 
|  |  | 133 |  |             { | 
|  | 14929 | 134 |  |                 foreach (var name in parameters.Keys.OrderBy(key => key, StringComparer.Ordinal)) | 
|  |  | 135 |  |                 { | 
|  |  | 136 |  | #pragma warning disable CA1308 // Normalize strings to uppercase | 
|  | 3055 | 137 |  |                     cr.Append('\n').Append(name.ToLowerInvariant()).Append(':').Append(parameters[name]); | 
|  |  | 138 |  | #pragma warning restore CA1308 // Normalize strings to uppercase | 
|  |  | 139 |  |                 } | 
|  |  | 140 |  |             } | 
|  | 3292 | 141 |  |             return cr.ToString(); | 
|  |  | 142 |  |         } | 
|  |  | 143 |  |     } | 
|  |  | 144 |  | } |