From a9f248ad817683e911348cd168c570db3d07757f Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 21 Apr 2019 00:08:59 +0800 Subject: Reorgnize api. Add basic unit test. --- Timeline/Configs/DatabaseConfig.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'Timeline/Configs') diff --git a/Timeline/Configs/DatabaseConfig.cs b/Timeline/Configs/DatabaseConfig.cs index 34e5e65f..05dc630e 100644 --- a/Timeline/Configs/DatabaseConfig.cs +++ b/Timeline/Configs/DatabaseConfig.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Timeline.Configs +namespace Timeline.Configs { public class DatabaseConfig { -- cgit v1.2.3 From 45c09ace4acee4df9c860395fb7261adb09bc914 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 22 Apr 2019 17:03:07 +0800 Subject: Add Tencent COS. --- Timeline/Configs/TencentCosConfig.cs | 13 ++++ Timeline/Services/TencentCloudCosService.cs | 94 +++++++++++++++++++++++++++++ Timeline/Startup.cs | 3 + Timeline/Timeline.csproj | 1 + 4 files changed, 111 insertions(+) create mode 100644 Timeline/Configs/TencentCosConfig.cs create mode 100644 Timeline/Services/TencentCloudCosService.cs (limited to 'Timeline/Configs') diff --git a/Timeline/Configs/TencentCosConfig.cs b/Timeline/Configs/TencentCosConfig.cs new file mode 100644 index 00000000..c41669f1 --- /dev/null +++ b/Timeline/Configs/TencentCosConfig.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +namespace Timeline.Configs +{ + public class TencentCosConfig + { + public string AppId { get; set; } + public string Region { get; set; } + public string SecretId { get; set; } + public string SecretKey { get; set; } + } +} diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs new file mode 100644 index 00000000..f1f52ec5 --- /dev/null +++ b/Timeline/Services/TencentCloudCosService.cs @@ -0,0 +1,94 @@ +using COSXML; +using COSXML.Auth; +using COSXML.CosException; +using COSXML.Model; +using COSXML.Model.Object; +using COSXML.Model.Tag; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Timeline.Configs; + +namespace Timeline.Services +{ + public interface ITencentCloudCosService + { + Task Exists(string bucket, string key); + string GetObjectUrl(string bucket, string key); + } + + public class TencentCloudCosService : ITencentCloudCosService + { + private readonly TencentCosConfig _config; + private readonly CosXmlServer _server; + private readonly ILogger _logger; + + public TencentCloudCosService(IOptions config, ILogger logger) + { + _config = config.Value; + _logger = logger; + + var cosConfig = new CosXmlConfig.Builder() + .IsHttps(true) + .SetAppid(config.Value.AppId) + .SetRegion(config.Value.Region) + .SetDebugLog(true) + .Build(); + + var credentialProvider = new DefaultQCloudCredentialProvider(config.Value.SecretId, config.Value.SecretKey, 3600); + + _server = new CosXmlServer(cosConfig, credentialProvider); + } + + public Task Exists(string bucket, string key) + { + bucket = bucket + "-" + _config.AppId; + + var request = new HeadObjectRequest(bucket, key); + + var t = new TaskCompletionSource(); + + _server.HeadObject(request, delegate (CosResult result) + { + if (result.httpCode >= 200 && result.httpCode < 300) + t.SetResult(true); + else + t.SetResult(false); + }, + delegate (CosClientException clientException, CosServerException serverException) + { + if (clientException != null) + { + _logger.LogError(clientException, "An client error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); + t.SetException(clientException); + return; + } + if (serverException != null) + { + _logger.LogError(serverException, "An server error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); + t.SetException(serverException); + return; + } + _logger.LogError("An unknown error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); + t.SetException(new Exception("Unknown exception when test cos object existence.")); + }); + + return t.Task; + } + + public string GetObjectUrl(string bucket, string key) + { + return _server.GenerateSignURL(new PreSignatureStruct() + { + appid = _config.AppId, + region = _config.Region, + bucket = bucket + "-" + _config.AppId, + key = key, + httpMethod = "GET", + isHttps = true, + signDurationSecond = 300 + }); + } + } +} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 285dfcfa..6491554a 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -80,6 +80,9 @@ namespace Timeline warnings.Throw(RelationalEventId.QueryClientEvaluationWarning); }); }); + + services.Configure(Configuration.GetSection(nameof(TencentCosConfig))); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 93513bd3..c9454ec9 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -12,5 +12,6 @@ + -- cgit v1.2.3 From 797b1da15c76f6598dcc48f675c1b82cb27a17ed Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 23 Apr 2019 00:14:35 +0800 Subject: Remove qcloud cs sdk. I will write one by myself. Develop signature algorithm. --- Timeline.Tests/QCloudCosServiceUnitTest.cs | 41 +++++++ Timeline/Configs/QCloudCosConfig.cs | 10 ++ Timeline/Configs/TencentCosConfig.cs | 13 --- Timeline/Services/TencentCloudCosService.cs | 174 +++++++++++++++++----------- Timeline/Services/UserService.cs | 6 +- Timeline/Startup.cs | 4 +- Timeline/Timeline.csproj | 1 - 7 files changed, 165 insertions(+), 84 deletions(-) create mode 100644 Timeline.Tests/QCloudCosServiceUnitTest.cs create mode 100644 Timeline/Configs/QCloudCosConfig.cs delete mode 100644 Timeline/Configs/TencentCosConfig.cs (limited to 'Timeline/Configs') diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs new file mode 100644 index 00000000..c02f70be --- /dev/null +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Timeline.Services; +using Xunit; + +namespace Timeline.Tests +{ + public class QCloudCosServiceUnitTest + { + [Fact] + public void GenerateSignatureTest() + { + var credential = new QCloudCosService.QCloudCredentials + { + SecretId = "AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q", + SecretKey = "BQYIM75p8x0iWVFSIgqEKwFprpRSVHlz" + }; + + var request = new QCloudCosService.RequestInfo + { + Method = "put", + Uri = "/exampleobject", + Parameters = new Dictionary(), + Headers = new Dictionary + { + ["Host"] = "examplebucket-1250000000.cos.ap-beijing.myqcloud.com", + ["x-cos-storage-class"] = "standard", + ["x-cos-content-sha1"] = "b502c3a1f48c8609ae212cdfb639dee39673f5e" + } + }; + + var signValidTime = new QCloudCosService.TimeDuration + { + Start = DateTimeOffset.FromUnixTimeSeconds(1417773892), + End = DateTimeOffset.FromUnixTimeSeconds(1417853898) + }; + + Assert.Equal("q-sign-algorithm=sha1&q-ak=AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q&q-sign-time=1417773892;1417853898&q-key-time=1417773892;1417853898&q-header-list=host;x-cos-content-sha1;x-cos-storage-class&q-url-param-list=&q-signature=0ab12f43e74cbe148d705cd9fae8adc9a6d39cc1", QCloudCosService.GenerateSign(credential, request, signValidTime)); + } + } +} diff --git a/Timeline/Configs/QCloudCosConfig.cs b/Timeline/Configs/QCloudCosConfig.cs new file mode 100644 index 00000000..6d10436c --- /dev/null +++ b/Timeline/Configs/QCloudCosConfig.cs @@ -0,0 +1,10 @@ +namespace Timeline.Configs +{ + public class QCloudCosConfig + { + public string AppId { get; set; } + public string Region { get; set; } + public string SecretId { get; set; } + public string SecretKey { get; set; } + } +} diff --git a/Timeline/Configs/TencentCosConfig.cs b/Timeline/Configs/TencentCosConfig.cs deleted file mode 100644 index c41669f1..00000000 --- a/Timeline/Configs/TencentCosConfig.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -namespace Timeline.Configs -{ - public class TencentCosConfig - { - public string AppId { get; set; } - public string Region { get; set; } - public string SecretId { get; set; } - public string SecretKey { get; set; } - } -} diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index 9ab9d54d..1bfcf745 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -1,99 +1,143 @@ -using COSXML; -using COSXML.Auth; -using COSXML.CosException; -using COSXML.Model; -using COSXML.Model.Object; -using COSXML.Model.Tag; -using Microsoft.Extensions.Logging; +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.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using Timeline.Configs; namespace Timeline.Services { - public interface ITencentCloudCosService + public interface IQCloudCosService { - Task Exists(string bucket, string key); + Task ObjectExists(string bucket, string key); string GetObjectUrl(string bucket, string key); } - public class TencentCloudCosService : ITencentCloudCosService + public class QCloudCosService : IQCloudCosService { - private readonly TencentCosConfig _config; - private readonly CosXmlServer _server; - private readonly ILogger _logger; + private readonly QCloudCosConfig _config; + private readonly ILogger _logger; - public TencentCloudCosService(IOptions config, ILogger logger) + public QCloudCosService(IOptions config, ILogger logger) { _config = config.Value; _logger = logger; + } - var cosConfig = new CosXmlConfig.Builder() - .IsHttps(true) - .SetAppid(config.Value.AppId) - .SetRegion(config.Value.Region) - .SetDebugLog(true) - .Build(); + public class QCloudCredentials + { + public string SecretId { get; set; } + public string SecretKey { get; set; } + } - var credentialProvider = new DefaultQCloudCredentialProvider(config.Value.SecretId, config.Value.SecretKey, 3600); + public class RequestInfo + { + public string Method { get; set; } + public string Uri { get; set; } + public IEnumerable> Parameters { get; set; } + public IEnumerable> Headers { get; set; } + } - _server = new CosXmlServer(cosConfig, credentialProvider); + public class TimeDuration + { + public DateTimeOffset Start { get; set; } + public DateTimeOffset End { get; set; } } - public Task Exists(string bucket, string key) + public static string GenerateSign(QCloudCredentials credentials, RequestInfo request, TimeDuration signValidTime) { - bucket = bucket + "-" + _config.AppId; + 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(request.Parameters != null); + Debug.Assert(request.Headers != 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) + { + 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)); - var request = new HeadObjectRequest(bucket, key); + 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)))); - var t = new TaskCompletionSource(); + HMACSHA1 hmac = new HMACSHA1(); - _server.HeadObject(request, delegate (CosResult result) + string ByteArrayToString(byte[] bytes) { - if (result.httpCode >= 200 && result.httpCode < 300) - t.TrySetResult(true); - else - t.TrySetResult(false); - }, - delegate (CosClientException clientException, CosServerException serverException) + 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) { - if (clientException != null) - { - _logger.LogError(clientException, "An client error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(clientException); - return; - } - if (serverException != null) - { - if (serverException.statusCode == 404) - { - t.TrySetResult(false); - return; - } - _logger.LogError(serverException, "An server error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(serverException); - return; - } - _logger.LogError("An unknown error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(new Exception("Unknown exception when test cos object existence.")); - }); - - return t.Task; + return string.Join('&', raw.Select(p => string.Concat(p.key, "=", p.value))); + } + + var httpString = new StringBuilder() + .Append(request.Method).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); + } + + public Task ObjectExists(string bucket, string key) + { + throw new NotImplementedException(); } public string GetObjectUrl(string bucket, string key) { - return _server.GenerateSignURL(new PreSignatureStruct() - { - appid = _config.AppId, - region = _config.Region, - bucket = bucket + "-" + _config.AppId, - key = key, - httpMethod = "GET", - isHttps = true, - signDurationSecond = 300 - }); + throw new NotImplementedException(); } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index a444d434..d1555660 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -150,9 +150,9 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; - private readonly ITencentCloudCosService _cosService; + private readonly IQCloudCosService _cosService; - public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, ITencentCloudCosService cosService) + public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService) { _logger = logger; _databaseContext = databaseContext; @@ -301,7 +301,7 @@ namespace Timeline.Services public async Task GetAvatarUrl(string username) { - var exists = await _cosService.Exists("avatar", username); + var exists = await _cosService.ObjectExists("avatar", username); if (exists) return _cosService.GetObjectUrl("avatar", username); else diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 6491554a..12d60843 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -81,8 +81,8 @@ namespace Timeline }); }); - services.Configure(Configuration.GetSection(nameof(TencentCosConfig))); - services.AddSingleton(); + services.Configure(Configuration.GetSection(nameof(QCloudCosConfig))); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index c9454ec9..93513bd3 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -12,6 +12,5 @@ - -- cgit v1.2.3