using Mogafa.Common; using Mogafa.Common.HttpClients; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace DidabuCloud.Unity.Core.NetWorking { public class DidabuSignatureHttpRequestHandler : MogafaBase, IHttpRequestHandler { readonly List signableHeaders = new List { "Content-Type", "Host", "X-Didabu-AppId", "X-Didabu-Version", "X-Didabu-Nonce", "X-Didabu-Timestamp", }; readonly Regex CompressWhitespaceRegex = new Regex("\\s+"); readonly HashAlgorithm CanonicalRequestHashAlgorithm = HashAlgorithm.Create("SHA-256"); private readonly string appId; private readonly string appKey; private readonly string version; public DidabuSignatureHttpRequestHandler(string appId, string appKey, string version) { this.appId = appId; this.appKey = appKey; this.version = version; } public int Order => 1000; public string Name => "DidabuSignature"; public void PrepareRequest(HttpRequest request) { var endpointUri = new Uri(request.Url); if (string.IsNullOrEmpty(request.Options.ContentType)) { request.Options.ContentType = "application/json"; } request.Options.Headers.Add("Content-Type", request.Options.ContentType); var queryParameters = endpointUri.Query; if (!string.IsNullOrEmpty(queryParameters)) { queryParameters = queryParameters.Substring(1); } var canonicalizedQueryParameters = string.Empty; if (!string.IsNullOrEmpty(queryParameters)) { var paramDictionary = queryParameters.Split('&').Select(p => p.Split('=')) .ToDictionary(nameval => nameval[0], nameval => nameval.Length > 1 ? nameval[1] : ""); var sb = new StringBuilder(); var paramKeys = new List(paramDictionary.Keys); paramKeys.Sort(StringComparer.Ordinal); foreach (var p in paramKeys) { if (sb.Length > 0) sb.Append("&"); sb.AppendFormat("{0}={1}", p, paramDictionary[p]); } canonicalizedQueryParameters = sb.ToString(); } var timestamp = Didabu.Application.RemoteUtcTimestamp;// (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000; request.Options.Headers.Add("X-Didabu-AppId", appId); request.Options.Headers.Add("X-Didabu-Version", version); request.Options.Headers.Add("X-Didabu-Timestamp", timestamp.ToString()); request.Options.Headers.Add("X-Didabu-Nonce", Guid.NewGuid().ToString()); var signHeaders = new Dictionary(); foreach(var keyValue in request.Options.Headers) { if (!signableHeaders.Contains(keyValue.Key)) { continue; } signHeaders.Add(keyValue.Key, keyValue.Value); } signHeaders.Add("Host", endpointUri.Host); var canonicalizedHeaderNames = CanonicalizeHeaderNames(signHeaders); var canonicalizedHeaders = CanonicalizeHeaders(signHeaders); if (string.IsNullOrEmpty(request.Body)) { request.Body = string.Empty; } var contentHash = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(request.Body)); var contentHashString = ToHexString(contentHash, true); var canonicalizedRequest = CanonicalizeRequest(endpointUri, request.Method.ToLower(), canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders, contentHashString); Logger.LogDebug($"appKey:{appKey}\ncanonicalizedRequest:{canonicalizedRequest}"); var signature = CreateSignature("HMACSHA256", appKey, canonicalizedRequest); request.Options.Headers.Add("X-Didabu-Signature", signature); Logger.LogDebug($"signature:{signature}"); } protected string CanonicalizeHeaderNames(IDictionary headers) { var headersToSign = new List(headers.Keys); headersToSign.Sort(StringComparer.OrdinalIgnoreCase); var sb = new StringBuilder(); foreach (var header in headersToSign) { if (sb.Length > 0) sb.Append(";"); sb.Append(header.ToLower()); } return sb.ToString(); } protected virtual string CanonicalizeHeaders(IDictionary headers) { if (headers == null || headers.Count == 0) return string.Empty; // step1: sort the headers into lower-case format; we create a new // map to ensure we can do a subsequent key lookup using a lower-case // key regardless of how 'headers' was created. var sortedHeaderMap = new SortedDictionary(); foreach (var header in headers.Keys) { sortedHeaderMap.Add(header.ToLower(), headers[header]); } // step2: form the canonical header:value entries in sorted order. // Multiple white spaces in the values should be compressed to a single // space. var sb = new StringBuilder(); foreach (var header in sortedHeaderMap.Keys) { var headerValue = CompressWhitespaceRegex.Replace(sortedHeaderMap[header], " "); sb.AppendFormat("{0}:{1}\n", header, headerValue.Trim()); } return sb.ToString(); } protected string CanonicalizeRequest(Uri endpointUri, string httpMethod, string queryParameters, string canonicalizedHeaderNames, string canonicalizedHeaders, string bodyHash) { var canonicalRequest = new StringBuilder(); canonicalRequest.AppendFormat("{0}\n", httpMethod); canonicalRequest.AppendFormat("{0}\n", CanonicalResourcePath(endpointUri)); canonicalRequest.AppendFormat("{0}\n", queryParameters); canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaders); canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaderNames); canonicalRequest.Append(bodyHash); return canonicalRequest.ToString(); } protected string CanonicalResourcePath(Uri endpointUri) { if (string.IsNullOrEmpty(endpointUri.AbsolutePath)) return "/"; // encode the path per RFC3986 return UrlEncode(endpointUri.AbsolutePath, true); } private string UrlEncode(string data, bool isPath = false) { // The Set of accepted and valid Url characters per RFC3986. Characters outside of this set will be encoded. const string validUrlCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; var encoded = new StringBuilder(data.Length * 2); string unreservedChars = String.Concat(validUrlCharacters, (isPath ? "/:" : "")); foreach (char symbol in System.Text.Encoding.UTF8.GetBytes(data)) { if (unreservedChars.IndexOf(symbol) != -1) encoded.Append(symbol); else encoded.Append("%").Append(String.Format("{0:X2}", (int)symbol)); } return encoded.ToString(); } string ToHexString(byte[] data, bool lowercase) { var sb = new StringBuilder(); for (var i = 0; i < data.Length; i++) { sb.Append(data[i].ToString(lowercase ? "x2" : "X2")); } return sb.ToString(); } protected byte[] ComputeKeyedHash(string algorithm, byte[] key, byte[] data) { var kha = KeyedHashAlgorithm.Create(algorithm); kha.Key = key; return kha.ComputeHash(data); } string CreateSignature(string algorithm, string key, string value) { char[] ksecret = $"DIDABU{key}".ToCharArray(); byte[] hashData = ComputeKeyedHash(algorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(value)); return ToHexString(hashData, true); } //protected byte[] DeriveSigningKey(string algorithm, string awsSecretAccessKey, string region, string date, string service) //{ // const string ksecretPrefix = SCHEME; // char[] ksecret = null; // ksecret = (ksecretPrefix + awsSecretAccessKey).ToCharArray(); // byte[] hashDate = ComputeKeyedHash(algorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date)); // byte[] hashRegion = ComputeKeyedHash(algorithm, hashDate, Encoding.UTF8.GetBytes(region)); // byte[] hashService = ComputeKeyedHash(algorithm, hashRegion, Encoding.UTF8.GetBytes(service)); // return ComputeKeyedHash(algorithm, hashService, Encoding.UTF8.GetBytes(TERMINATOR)); //} } }