From 214ceda8903bafc28981b58a0530c2a15d7812cc Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 25 Apr 2019 19:03:46 +0800 Subject: Rename the cos service file. Fix a bug in test. --- Timeline.Tests/QCloudCosServiceUnitTest.cs | 7 +- Timeline/Services/QCloudCosService.cs | 257 ++++++++++++++++++++++++++++ Timeline/Services/TencentCloudCosService.cs | 257 ---------------------------- 3 files changed, 261 insertions(+), 260 deletions(-) create mode 100644 Timeline/Services/QCloudCosService.cs delete mode 100644 Timeline/Services/TencentCloudCosService.cs diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs index b0e6a868..0940c70d 100644 --- a/Timeline.Tests/QCloudCosServiceUnitTest.cs +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using Timeline.Services; using Timeline.Tests.Helpers; @@ -80,8 +81,6 @@ namespace Timeline.Tests } } - // Although this test does not pass on my archlunux system. But the GenerateObjectGetUrl actually works well. - // And I don't know why. [Fact] public async Task GenerateObjectGetUrlTest() { @@ -90,7 +89,9 @@ namespace Timeline.Tests var services = serviceScope.ServiceProvider; var service = services.GetRequiredService(); var url = service.GenerateObjectGetUrl("avatar", "__default"); - using (var client = _factory.CreateClient()) + // never use the following line! Because client created by factory can't access Internet. + //using (var client = _factory.CreateClient()) + using (var client = services.GetRequiredService().CreateClient()) { var res = await client.GetAsync(url); Assert.Equal(HttpStatusCode.OK, res.StatusCode); diff --git a/Timeline/Services/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs new file mode 100644 index 00000000..f4358714 --- /dev/null +++ b/Timeline/Services/QCloudCosService.cs @@ -0,0 +1,257 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Timeline.Configs; + +namespace Timeline.Services +{ + public interface IQCloudCosService + { + /// + /// Test if an object in the bucket exists. + /// + /// The bucket name. + /// The object key. + /// True if exists. False if not. + Task IsObjectExists(string bucket, string key); + + /// + /// Generate a presignated url to access the object. + /// + /// The bucket name. + /// The object key. + /// The presignated url. + string GenerateObjectGetUrl(string bucket, string key); + } + + public class QCloudCosService : IQCloudCosService + { + private readonly IOptionsMonitor _config; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public QCloudCosService(IOptionsMonitor config, ILogger logger, IHttpClientFactory httpClientFactory) + { + _config = config; + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + private const string BucketNamePattern = @"^(([a-z0-9][a-z0-9-]*[a-z0-9])|[a-z0-9])$"; + + public static bool ValidateBucketName(string bucketName) + { + return Regex.IsMatch(bucketName, BucketNamePattern); + } + + public class QCloudCredentials + { + public string SecretId { get; set; } + public string SecretKey { get; set; } + } + + public class RequestInfo + { + public string Method { get; set; } + public string Uri { get; set; } + public IEnumerable> Parameters { get; set; } + public IEnumerable> Headers { get; set; } + } + + public class TimeDuration + { + public TimeDuration() + { + + } + + public TimeDuration(DateTimeOffset start, DateTimeOffset end) + { + Start = start; + End = end; + } + + public DateTimeOffset Start { get; set; } + public DateTimeOffset End { get; set; } + } + + public static string GenerateSign(QCloudCredentials credentials, RequestInfo request, TimeDuration signValidTime) + { + Debug.Assert(credentials != null); + Debug.Assert(credentials.SecretId != null); + Debug.Assert(credentials.SecretKey != null); + Debug.Assert(request != null); + Debug.Assert(request.Method != null); + Debug.Assert(request.Uri != null); + Debug.Assert(signValidTime != null); + Debug.Assert(signValidTime.Start < signValidTime.End, "Start must be before End in sign valid time."); + + List<(string key, string value)> Transform(IEnumerable> raw) + { + if (raw == null) + return new List<(string key, string value)>(); + + var sorted = raw.Select(p => (key: p.Key.ToLower(), value: WebUtility.UrlEncode(p.Value))).ToList(); + sorted.Sort((left, right) => string.CompareOrdinal(left.key, right.key)); + return sorted; + } + + var transformedParameters = Transform(request.Parameters); + var transformedHeaders = Transform(request.Headers); + + List<(string, string)> result = new List<(string, string)>(); + + const string signAlgorithm = "sha1"; + result.Add(("q-sign-algorithm", signAlgorithm)); + + result.Add(("q-ak", credentials.SecretId)); + + var signTime = $"{signValidTime.Start.ToUnixTimeSeconds().ToString()};{signValidTime.End.ToUnixTimeSeconds().ToString()}"; + var keyTime = signTime; + result.Add(("q-sign-time", signTime)); + result.Add(("q-key-time", keyTime)); + + result.Add(("q-header-list", string.Join(';', transformedHeaders.Select(h => h.key)))); + result.Add(("q-url-param-list", string.Join(';', transformedParameters.Select(p => p.key)))); + + HMACSHA1 hmac = new HMACSHA1(); + + string ByteArrayToString(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", "").ToLower(); + } + + hmac.Key = Encoding.UTF8.GetBytes(credentials.SecretKey); + var signKey = ByteArrayToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(keyTime))); + + string Join(IEnumerable<(string key, string value)> raw) + { + return string.Join('&', raw.Select(p => string.Concat(p.key, "=", p.value))); + } + + var httpString = new StringBuilder() + .Append(request.Method.ToLower()).Append('\n') + .Append(request.Uri).Append('\n') + .Append(Join(transformedParameters)).Append('\n') + .Append(Join(transformedHeaders)).Append('\n') + .ToString(); + + string Sha1(string data) + { + var sha1 = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(data)); + return ByteArrayToString(sha1); + } + + var stringToSign = new StringBuilder() + .Append(signAlgorithm).Append('\n') + .Append(signTime).Append('\n') + .Append(Sha1(httpString)).Append('\n') + .ToString(); + + hmac.Key = Encoding.UTF8.GetBytes(signKey); + var signature = ByteArrayToString(hmac.ComputeHash( + Encoding.UTF8.GetBytes(stringToSign))); + + result.Add(("q-signature", signature)); + + return Join(result); + } + + private QCloudCredentials GetCredentials() + { + var config = _config.CurrentValue; + return new QCloudCredentials + { + SecretId = config.SecretId, + SecretKey = config.SecretKey + }; + } + + private string GetHost(string bucket) + { + var config = _config.CurrentValue; + return $"{bucket}-{config.AppId}.cos.{config.Region}.myqcloud.com"; + } + + public async Task IsObjectExists(string bucket, string key) + { + if (bucket == null) + throw new ArgumentNullException(nameof(bucket)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (!ValidateBucketName(bucket)) + throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); + + var client = _httpClientFactory.CreateClient(); + + var host = GetHost(bucket); + var encodedKey = WebUtility.UrlEncode(key); + + var request = new HttpRequestMessage(); + request.Method = HttpMethod.Head; + request.RequestUri = new Uri($"https://{host}/{encodedKey}"); + request.Headers.Host = host; + request.Headers.Date = DateTimeOffset.Now; + request.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo + { + Method = "head", + Uri = "/" + encodedKey, + Headers = new Dictionary + { + ["Host"] = host + } + }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(2)))); + + try + { + var response = await client.SendAsync(request); + + if (response.IsSuccessStatusCode) + return true; + if (response.StatusCode == HttpStatusCode.NotFound) + return false; + + throw new Exception($"Unknown response code. {response.ToString()}"); + } + catch (Exception e) + { + _logger.LogError(e, "An error occured when test a cos object existence."); + return false; + } + } + + public string GenerateObjectGetUrl(string bucket, string key) + { + if (bucket == null) + throw new ArgumentNullException(nameof(bucket)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (!ValidateBucketName(bucket)) + throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); + + var host = GetHost(bucket); + var encodedKey = WebUtility.UrlEncode(key); + + var signature = GenerateSign(GetCredentials(), new RequestInfo + { + Method = "get", + Uri = "/" + encodedKey, + Headers = new Dictionary + { + ["Host"] = host + } + }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(6))); + + return $"https://{host}/{encodedKey}?{signature}"; + } + } +} diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs deleted file mode 100644 index f4358714..00000000 --- a/Timeline/Services/TencentCloudCosService.cs +++ /dev/null @@ -1,257 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Timeline.Configs; - -namespace Timeline.Services -{ - public interface IQCloudCosService - { - /// - /// Test if an object in the bucket exists. - /// - /// The bucket name. - /// The object key. - /// True if exists. False if not. - Task IsObjectExists(string bucket, string key); - - /// - /// Generate a presignated url to access the object. - /// - /// The bucket name. - /// The object key. - /// The presignated url. - string GenerateObjectGetUrl(string bucket, string key); - } - - public class QCloudCosService : IQCloudCosService - { - private readonly IOptionsMonitor _config; - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - - public QCloudCosService(IOptionsMonitor config, ILogger logger, IHttpClientFactory httpClientFactory) - { - _config = config; - _logger = logger; - _httpClientFactory = httpClientFactory; - } - - private const string BucketNamePattern = @"^(([a-z0-9][a-z0-9-]*[a-z0-9])|[a-z0-9])$"; - - public static bool ValidateBucketName(string bucketName) - { - return Regex.IsMatch(bucketName, BucketNamePattern); - } - - public class QCloudCredentials - { - public string SecretId { get; set; } - public string SecretKey { get; set; } - } - - public class RequestInfo - { - public string Method { get; set; } - public string Uri { get; set; } - public IEnumerable> Parameters { get; set; } - public IEnumerable> Headers { get; set; } - } - - public class TimeDuration - { - public TimeDuration() - { - - } - - public TimeDuration(DateTimeOffset start, DateTimeOffset end) - { - Start = start; - End = end; - } - - public DateTimeOffset Start { get; set; } - public DateTimeOffset End { get; set; } - } - - public static string GenerateSign(QCloudCredentials credentials, RequestInfo request, TimeDuration signValidTime) - { - Debug.Assert(credentials != null); - Debug.Assert(credentials.SecretId != null); - Debug.Assert(credentials.SecretKey != null); - Debug.Assert(request != null); - Debug.Assert(request.Method != null); - Debug.Assert(request.Uri != null); - Debug.Assert(signValidTime != null); - Debug.Assert(signValidTime.Start < signValidTime.End, "Start must be before End in sign valid time."); - - List<(string key, string value)> Transform(IEnumerable> raw) - { - if (raw == null) - return new List<(string key, string value)>(); - - var sorted = raw.Select(p => (key: p.Key.ToLower(), value: WebUtility.UrlEncode(p.Value))).ToList(); - sorted.Sort((left, right) => string.CompareOrdinal(left.key, right.key)); - return sorted; - } - - var transformedParameters = Transform(request.Parameters); - var transformedHeaders = Transform(request.Headers); - - List<(string, string)> result = new List<(string, string)>(); - - const string signAlgorithm = "sha1"; - result.Add(("q-sign-algorithm", signAlgorithm)); - - result.Add(("q-ak", credentials.SecretId)); - - var signTime = $"{signValidTime.Start.ToUnixTimeSeconds().ToString()};{signValidTime.End.ToUnixTimeSeconds().ToString()}"; - var keyTime = signTime; - result.Add(("q-sign-time", signTime)); - result.Add(("q-key-time", keyTime)); - - result.Add(("q-header-list", string.Join(';', transformedHeaders.Select(h => h.key)))); - result.Add(("q-url-param-list", string.Join(';', transformedParameters.Select(p => p.key)))); - - HMACSHA1 hmac = new HMACSHA1(); - - string ByteArrayToString(byte[] bytes) - { - return BitConverter.ToString(bytes).Replace("-", "").ToLower(); - } - - hmac.Key = Encoding.UTF8.GetBytes(credentials.SecretKey); - var signKey = ByteArrayToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(keyTime))); - - string Join(IEnumerable<(string key, string value)> raw) - { - return string.Join('&', raw.Select(p => string.Concat(p.key, "=", p.value))); - } - - var httpString = new StringBuilder() - .Append(request.Method.ToLower()).Append('\n') - .Append(request.Uri).Append('\n') - .Append(Join(transformedParameters)).Append('\n') - .Append(Join(transformedHeaders)).Append('\n') - .ToString(); - - string Sha1(string data) - { - var sha1 = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(data)); - return ByteArrayToString(sha1); - } - - var stringToSign = new StringBuilder() - .Append(signAlgorithm).Append('\n') - .Append(signTime).Append('\n') - .Append(Sha1(httpString)).Append('\n') - .ToString(); - - hmac.Key = Encoding.UTF8.GetBytes(signKey); - var signature = ByteArrayToString(hmac.ComputeHash( - Encoding.UTF8.GetBytes(stringToSign))); - - result.Add(("q-signature", signature)); - - return Join(result); - } - - private QCloudCredentials GetCredentials() - { - var config = _config.CurrentValue; - return new QCloudCredentials - { - SecretId = config.SecretId, - SecretKey = config.SecretKey - }; - } - - private string GetHost(string bucket) - { - var config = _config.CurrentValue; - return $"{bucket}-{config.AppId}.cos.{config.Region}.myqcloud.com"; - } - - public async Task IsObjectExists(string bucket, string key) - { - if (bucket == null) - throw new ArgumentNullException(nameof(bucket)); - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (!ValidateBucketName(bucket)) - throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); - - var client = _httpClientFactory.CreateClient(); - - var host = GetHost(bucket); - var encodedKey = WebUtility.UrlEncode(key); - - var request = new HttpRequestMessage(); - request.Method = HttpMethod.Head; - request.RequestUri = new Uri($"https://{host}/{encodedKey}"); - request.Headers.Host = host; - request.Headers.Date = DateTimeOffset.Now; - request.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo - { - Method = "head", - Uri = "/" + encodedKey, - Headers = new Dictionary - { - ["Host"] = host - } - }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(2)))); - - try - { - var response = await client.SendAsync(request); - - if (response.IsSuccessStatusCode) - return true; - if (response.StatusCode == HttpStatusCode.NotFound) - return false; - - throw new Exception($"Unknown response code. {response.ToString()}"); - } - catch (Exception e) - { - _logger.LogError(e, "An error occured when test a cos object existence."); - return false; - } - } - - public string GenerateObjectGetUrl(string bucket, string key) - { - if (bucket == null) - throw new ArgumentNullException(nameof(bucket)); - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (!ValidateBucketName(bucket)) - throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); - - var host = GetHost(bucket); - var encodedKey = WebUtility.UrlEncode(key); - - var signature = GenerateSign(GetCredentials(), new RequestInfo - { - Method = "get", - Uri = "/" + encodedKey, - Headers = new Dictionary - { - ["Host"] = host - } - }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(6))); - - return $"https://{host}/{encodedKey}?{signature}"; - } - } -} -- cgit v1.2.3