| | 1 | | using System.Net.Http.Headers; |
| | 2 | | using System.Security.Cryptography; |
| | 3 | |
|
| | 4 | | namespace Songhay.Extensions; |
| | 5 | |
|
| | 6 | | public static partial class HttpRequestMessageExtensions |
| | 7 | | { |
| | 8 | | /// <summary> |
| | 9 | | /// Derives the <see cref="AuthenticationHeaderValue"/> |
| | 10 | | /// from the <see cref="HttpRequestMessage"/>. |
| | 11 | | /// </summary> |
| | 12 | | /// <param name="request">the <see cref="HttpRequestMessage"/></param> |
| | 13 | | /// <param name="storageAccountName">the Azure Storage account name</param> |
| | 14 | | /// <param name="storageAccountKey">the Azure Storage account shared key</param> |
| | 15 | | /// <param name="eTag">entity tag for Web cache validation</param> |
| | 16 | | /// <param name="md5">The MD5 (message-digest algorithm) hash</param> |
| | 17 | | /// <remarks> |
| | 18 | | /// There are two Authorization Header schemes supported: SharedKey and SharedKeyLite. This member supports only one |
| | 19 | | /// For more detail, see “Specifying the Authorization header” |
| | 20 | | /// [ https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#specifying-the-authorizati |
| | 21 | | /// |
| | 22 | | /// See also: “Authorize requests to Azure Storage” |
| | 23 | | /// [ https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage ] |
| | 24 | | /// |
| | 25 | | /// See also: https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth/tree/master |
| | 26 | | /// |
| | 27 | | /// Provide the md5 and it will check and make sure it matches the requested blob's md5. |
| | 28 | | /// If it doesn't match, it won't return a value. |
| | 29 | | /// |
| | 30 | | /// Provide an eTag, and it will only make changes to a blob if the current eTag matches, |
| | 31 | | /// to ensure you don't overwrite someone else's changes. |
| | 32 | | /// </remarks> |
| | 33 | | public static AuthenticationHeaderValue ToAzureStorageAuthorizationHeader(this HttpRequestMessage? request, |
| | 34 | | string? storageAccountName, string? storageAccountKey, string? eTag, string? md5) |
| 0 | 35 | | { |
| 0 | 36 | | ArgumentNullException.ThrowIfNull(request); |
| 0 | 37 | | storageAccountKey.ThrowWhenNullOrWhiteSpace(); |
| | 38 | |
|
| 0 | 39 | | var signatureBytes = request.ToAzureStorageSignature(storageAccountName, eTag, md5); |
| | 40 | |
|
| 0 | 41 | | var sha256 = new HMACSHA256(Convert.FromBase64String(storageAccountKey)); |
| | 42 | |
|
| | 43 | | const string scheme = "SharedKey"; |
| 0 | 44 | | var parameter = $"{storageAccountName}:{Convert.ToBase64String(sha256.ComputeHash(signatureBytes))}"; |
| | 45 | |
|
| 0 | 46 | | var value = new AuthenticationHeaderValue(scheme, parameter); |
| | 47 | |
|
| 0 | 48 | | return value; |
| 0 | 49 | | } |
| | 50 | |
|
| | 51 | | /// <summary> |
| | 52 | | /// Returns headers, starting with <c>x-ms-</c>, |
| | 53 | | /// in a canonical format. |
| | 54 | | /// </summary> |
| | 55 | | /// <param name="request">the <see cref="HttpRequestMessage"/></param> |
| | 56 | | /// <remarks> |
| | 57 | | /// See https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth/tree/master |
| | 58 | | /// |
| | 59 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage |
| | 60 | | /// |
| | 61 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key |
| | 62 | | /// |
| | 63 | | /// See http://en.wikipedia.org/wiki/Canonicalization |
| | 64 | | /// </remarks> |
| | 65 | | public static string ToAzureStorageCanonicalizedHeaders(this HttpRequestMessage? request) |
| 0 | 66 | | { |
| 0 | 67 | | ArgumentNullException.ThrowIfNull(request); |
| | 68 | |
|
| 0 | 69 | | var xMsHeaders = request.Headers |
| 0 | 70 | | .Where(pair => pair.Key.StartsWith("x-ms-", StringComparison.OrdinalIgnoreCase)) |
| 0 | 71 | | .OrderBy(pair => pair.Key) |
| 0 | 72 | | .Select(pair => new {Key = pair.Key.ToLowerInvariant(), pair.Value}); |
| | 73 | |
|
| 0 | 74 | | var sb = new StringBuilder(); |
| | 75 | |
|
| 0 | 76 | | foreach (var pair in xMsHeaders) |
| 0 | 77 | | { |
| 0 | 78 | | var innerBuilder = new StringBuilder(pair.Key); |
| 0 | 79 | | char separator = ':'; |
| | 80 | |
|
| 0 | 81 | | foreach (string headerValues in pair.Value) |
| 0 | 82 | | { |
| 0 | 83 | | string trimmedValue = headerValues.TrimStart().Replace("\r\n", String.Empty); |
| 0 | 84 | | innerBuilder.Append(separator).Append(trimmedValue); |
| | 85 | |
|
| | 86 | | // Set this to a comma; this will only be used |
| | 87 | | // if there are multiple values for one of the headers. |
| 0 | 88 | | separator = ','; |
| 0 | 89 | | } |
| | 90 | |
|
| 0 | 91 | | sb.Append(innerBuilder.ToString()).Append("\n"); |
| 0 | 92 | | } |
| | 93 | |
|
| 0 | 94 | | return sb.ToString(); |
| 0 | 95 | | } |
| | 96 | |
|
| | 97 | | /// <summary> |
| | 98 | | /// Derives the raw representation of the message signature |
| | 99 | | /// from the <see cref="HttpRequestMessage"/>.` |
| | 100 | | /// </summary> |
| | 101 | | /// <param name="request">the <see cref="HttpRequestMessage"/></param> |
| | 102 | | /// <param name="storageAccountName">The name of the storage account to use.</param> |
| | 103 | | /// <param name="eTag">entity tag for Web cache validation</param> |
| | 104 | | /// <param name="md5">The MD5 (message-digest algorithm) hash</param> |
| | 105 | | /// <remarks> |
| | 106 | | /// See https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth/tree/master |
| | 107 | | /// |
| | 108 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage |
| | 109 | | /// |
| | 110 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key |
| | 111 | | /// </remarks> |
| | 112 | | public static byte[] ToAzureStorageSignature(this HttpRequestMessage? request, string? storageAccountName, |
| | 113 | | string? eTag, string? md5) |
| 0 | 114 | | { |
| 0 | 115 | | ArgumentNullException.ThrowIfNull(request); |
| 0 | 116 | | if (request.Content == null) |
| 0 | 117 | | throw new NullReferenceException($"{nameof(request)}.{nameof(request.Content)}"); |
| 0 | 118 | | if (string.IsNullOrWhiteSpace(eTag)) eTag = string.Empty; |
| 0 | 119 | | if (string.IsNullOrWhiteSpace(md5)) md5 = string.Empty; |
| | 120 | |
|
| 0 | 121 | | var contentLength = string.Empty; |
| | 122 | |
|
| 0 | 123 | | HttpMethod method = request.Method; |
| | 124 | |
|
| 0 | 125 | | if (method == HttpMethod.Put) |
| 0 | 126 | | { |
| | 127 | | try |
| 0 | 128 | | { |
| 0 | 129 | | contentLength = request.Content.Headers.ContentLength.ToString(); |
| 0 | 130 | | } |
| 0 | 131 | | catch (NullReferenceException ex) |
| 0 | 132 | | { |
| 0 | 133 | | throw new NullReferenceException( |
| 0 | 134 | | $"The expected Content Headers are not here. Was {nameof(HttpRequestMessage)}.{nameof(HttpRequestMes |
| 0 | 135 | | ex); |
| | 136 | | } |
| 0 | 137 | | } |
| | 138 | |
|
| 0 | 139 | | string canonicalizedHeaders = request.ToAzureStorageCanonicalizedHeaders(); |
| 0 | 140 | | string location = request.RequestUri.ToAzureStorageCanonicalizedResourceLocation(storageAccountName); |
| | 141 | |
|
| 0 | 142 | | var messageSignature = |
| 0 | 143 | | $"{method}\n\n\n{contentLength}\n{md5}\n\n\n\n{eTag}\n\n\n\n{canonicalizedHeaders}{location}"; |
| | 144 | |
|
| 0 | 145 | | byte[] signatureBytes = Encoding.UTF8.GetBytes(messageSignature); |
| | 146 | |
|
| 0 | 147 | | return signatureBytes; |
| 0 | 148 | | } |
| | 149 | |
|
| | 150 | | /// <summary> |
| | 151 | | /// Returns <see cref="HttpRequestMessage"/> |
| | 152 | | /// with conventional headers for <see cref="ByteArrayContent"/> |
| | 153 | | /// for Azure Storage. |
| | 154 | | /// </summary> |
| | 155 | | /// <param name="request">the <see cref="HttpRequestMessage"/></param> |
| | 156 | | /// <param name="blobName">the Azure Storage Blob name</param> |
| | 157 | | /// <param name="content">the Azure Storage Blob content</param> |
| | 158 | | public static HttpRequestMessage WithAzureStorageBlockBlobContent(this HttpRequestMessage? request, |
| | 159 | | string? blobName, string? content) |
| 0 | 160 | | { |
| 0 | 161 | | ArgumentNullException.ThrowIfNull(request); |
| 0 | 162 | | ArgumentNullException.ThrowIfNull(blobName); |
| 0 | 163 | | ArgumentNullException.ThrowIfNull(content); |
| | 164 | |
|
| 0 | 165 | | byte[] bytes = Encoding.UTF8.GetBytes(content); |
| | 166 | |
|
| 0 | 167 | | request.Content = new ByteArrayContent(bytes); |
| | 168 | |
|
| 0 | 169 | | request.Headers.Add("x-ms-blob-content-disposition", $@"attachment; filename=""{blobName}"""); |
| 0 | 170 | | request.Headers.Add("x-ms-blob-type", "BlockBlob"); |
| 0 | 171 | | request.Headers.Add("x-ms-meta-m1", "v1"); |
| 0 | 172 | | request.Headers.Add("x-ms-meta-m2", "v2"); |
| | 173 | |
|
| 0 | 174 | | return request; |
| 0 | 175 | | } |
| | 176 | |
|
| | 177 | | /// <summary> |
| | 178 | | /// Returns <see cref="HttpRequestMessage"/> |
| | 179 | | /// with conventional headers for Azure Storage. |
| | 180 | | /// </summary> |
| | 181 | | /// <param name="request"></param> |
| | 182 | | /// <param name="requestMoment"></param> |
| | 183 | | /// <param name="serviceVersion"></param> |
| | 184 | | /// <param name="storageAccountName"></param> |
| | 185 | | /// <param name="storageAccountKey"></param> |
| | 186 | | public static HttpRequestMessage WithAzureStorageHeaders(this HttpRequestMessage? request, |
| | 187 | | DateTime requestMoment, string? serviceVersion, string? storageAccountName, string? storageAccountKey) |
| 0 | 188 | | { |
| 0 | 189 | | return request.WithAzureStorageHeaders( |
| 0 | 190 | | requestMoment, |
| 0 | 191 | | serviceVersion, |
| 0 | 192 | | storageAccountName, |
| 0 | 193 | | storageAccountKey, |
| 0 | 194 | | eTag: null, |
| 0 | 195 | | md5: null |
| 0 | 196 | | ); |
| 0 | 197 | | } |
| | 198 | |
|
| | 199 | | /// <summary> |
| | 200 | | /// Returns <see cref="HttpRequestMessage"/> with the minimum headers |
| | 201 | | /// required for Azure Storage. |
| | 202 | | /// </summary> |
| | 203 | | /// <param name="request">the <see cref="HttpRequestMessage"/></param> |
| | 204 | | /// <param name="requestMoment">the moment of the request</param> |
| | 205 | | /// <param name="serviceVersion"></param> |
| | 206 | | /// <param name="storageAccountName">the Azure Storage account name</param> |
| | 207 | | /// <param name="storageAccountKey">the Azure Storage account shared key</param> |
| | 208 | | /// <param name="eTag">entity tag for Web cache validation</param> |
| | 209 | | /// <param name="md5">The MD5 (message-digest algorithm) hash</param> |
| | 210 | | /// <remarks> |
| | 211 | | /// See https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth/tree/master |
| | 212 | | /// |
| | 213 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-requests-to-azure-storage |
| | 214 | | /// |
| | 215 | | /// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key |
| | 216 | | /// |
| | 217 | | /// Provide the md5 and it will check and make sure it matches the requested blob's md5. If it doesn't match, it won |
| | 218 | | /// |
| | 219 | | /// Provide an eTag, and it will only make changes to a blob if the current eTag matches, to ensure you don't overwr |
| | 220 | | /// </remarks> |
| | 221 | | public static HttpRequestMessage WithAzureStorageHeaders(this HttpRequestMessage? request, |
| | 222 | | DateTime requestMoment, string? serviceVersion, string? storageAccountName, string? storageAccountKey, |
| | 223 | | string? eTag, string? md5) |
| 0 | 224 | | { |
| 0 | 225 | | ArgumentNullException.ThrowIfNull(request); |
| | 226 | |
|
| 0 | 227 | | request.Headers.Add("x-ms-date", requestMoment.ToString("R", CultureInfo.InvariantCulture)); |
| 0 | 228 | | request.Headers.Add("x-ms-version", serviceVersion); |
| | 229 | |
|
| 0 | 230 | | request.Headers.Authorization = |
| 0 | 231 | | request.ToAzureStorageAuthorizationHeader( |
| 0 | 232 | | storageAccountName, |
| 0 | 233 | | storageAccountKey, |
| 0 | 234 | | eTag, md5); |
| | 235 | |
|
| 0 | 236 | | return request; |
| 0 | 237 | | } |
| | 238 | | } |