/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using Amazon.Runtime; using Amazon.Runtime.Internal.Auth; using Amazon.Util; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace OpenSearch.Net.Auth.AwsSigV4 { public class CanonicalRequest { private static readonly IComparer> StringPairComparer = new KeyValuePairComparer(); private static readonly ISet ExcludedHeaders = new HashSet { HeaderNames.UserAgent, HeaderNames.ContentLength }; private readonly string _method; private readonly string _path; private readonly string _params; private readonly SortedDictionary> _headers; public string XAmzContentSha256 { get; } public string XAmzDate { get; } public string XAmzSecurityToken { get; } public string SignedHeaders { get; } private CanonicalRequest(string method, string path, string queryParams, SortedDictionary> headers, string xAmzContentSha256, string xAmzDate, string xAmzSecurityToken ) { _method = method; _path = path; _params = queryParams; _headers = headers; XAmzContentSha256 = xAmzContentSha256; SignedHeaders = string.Join(";", _headers.Keys); XAmzDate = xAmzDate; XAmzSecurityToken = xAmzSecurityToken; } public static async Task From(HttpRequestMessage request, ImmutableCredentials credentials, DateTime signingTime) { var path = AWSSDKUtils.CanonicalizeResourcePathV2(request.RequestUri, null, false, null); var bodyBytes = await GetBodyBytes(request).ConfigureAwait(false); var xAmzContentSha256 = AWSSDKUtils.ToHex(AWS4Signer.ComputeHash(bodyBytes), true); var xAmzDate = AWS4Signer.FormatDateTime(signingTime, "yyyyMMddTHHmmssZ"); var canonicalHeaders = new SortedDictionary>(); CanonicalizeHeaders(canonicalHeaders, request.Headers); CanonicalizeHeaders(canonicalHeaders, request.Content?.Headers); canonicalHeaders[HeaderNames.Host] = new List { request.RequestUri.Authority }; canonicalHeaders[HeaderNames.XAmzDate] = new List { xAmzDate }; canonicalHeaders[HeaderNames.XAmzContentSha256] = new List { xAmzContentSha256 }; string xAmzSecurityToken = null; if (credentials.UseToken) { xAmzSecurityToken = credentials.Token; canonicalHeaders[HeaderNames.XAmzSecurityToken] = new List { xAmzSecurityToken }; } var queryParams = HttpUtility.ParseQueryString(request.RequestUri.Query); var orderedParams = queryParams .AllKeys .SelectMany(k => queryParams.GetValues(k) ?.Select(v => !string.IsNullOrEmpty(k) ? new KeyValuePair(k, v) : new KeyValuePair(v, string.Empty)) ?? Enumerable.Empty>()) .OrderBy(pair => pair, StringPairComparer) .Select(pair => $"{AWSSDKUtils.UrlEncode(pair.Key, false)}={AWSSDKUtils.UrlEncode(pair.Value, false)}"); var paramString = string.Join("&", orderedParams); var method = request.Method.ToString(); return new CanonicalRequest(method, path, paramString, canonicalHeaders, xAmzContentSha256, xAmzDate, xAmzSecurityToken); } private static async Task GetBodyBytes(HttpRequestMessage request) { if (request.Content == null) return Array.Empty(); var body = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); if (request.Content is ByteArrayContent) return body; var content = new ByteArrayContent(body); foreach (var pair in request.Content.Headers) { if (string.Equals(pair.Key, HeaderNames.ContentLength, StringComparison.OrdinalIgnoreCase)) continue; content.Headers.TryAddWithoutValidation(pair.Key, pair.Value); } request.Content = content; return body; } private static void CanonicalizeHeaders( IDictionary> canonicalHeaders, HttpHeaders headers ) { if (headers == null) return; foreach (var pair in headers) { if (pair.Value == null) continue; var key = pair.Key.ToLowerInvariant(); if (ExcludedHeaders.Contains(key)) continue; if (!canonicalHeaders.TryGetValue(key, out var dictValues)) dictValues = canonicalHeaders[key] = new List(); dictValues.AddRange(pair.Value.Select(v => AWSSDKUtils.CompressSpaces(v).Trim())); } } public override string ToString() { var sb = new StringBuilder(); sb.Append($"{_method}\n"); sb.Append($"{_path}\n"); sb.Append($"{_params}\n"); foreach (var header in _headers) sb.Append($"{header.Key}:{string.Join(",", header.Value)}\n"); sb.Append('\n'); sb.Append($"{SignedHeaders}\n"); sb.Append(XAmzContentSha256); return sb.ToString(); } private class KeyValuePairComparer : IComparer> where TKey : IComparable where TValue : IComparable { public int Compare(KeyValuePair x, KeyValuePair y) { var keyComparison = x.Key.CompareTo(y.Key); return keyComparison != 0 ? keyComparison : x.Value.CompareTo(y.Value); } } } }