From 076e0131dff71a9f76fff13c92fffa0ef408935f 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.Tests/AuthorizationUnitTest.cs | 14 +++---- .../Authentication/AuthenticationExtensions.cs | 47 ++++++++++++++++++++++ .../AuthenticationHttpClientExtensions.cs | 38 ----------------- Timeline.Tests/Helpers/TestUsers.cs | 40 ++++++++++++++++++ .../Helpers/WebApplicationFactoryExtensions.cs | 21 +--------- Timeline.Tests/JwtTokenUnitTest.cs | 4 +- Timeline.Tests/Timeline.Tests.csproj | 2 +- Timeline.Tests/UserUnitTest.cs | 36 +++++++++++++++++ 8 files changed, 133 insertions(+), 69 deletions(-) create mode 100644 Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs delete mode 100644 Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs create mode 100644 Timeline.Tests/Helpers/TestUsers.cs create mode 100644 Timeline.Tests/UserUnitTest.cs (limited to 'Timeline.Tests') diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index e450af06..28715ada 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -26,7 +26,7 @@ namespace Timeline.Tests { using (var client = _factory.CreateDefaultClient()) { - var response = await client.GetAsync(NeedAuthorizeUrl); + var response = await client.GetAsync(NeedAuthorizeUrl); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } } @@ -34,10 +34,9 @@ namespace Timeline.Tests [Fact] public async Task AuthenticationTest() { - using (var client = _factory.CreateDefaultClient()) + using (var client = await _factory.CreateClientWithUser("user", "user")) { - var token = (await client.CreateUserTokenAsync("user", "user")).Token; - var response = await client.SendWithAuthenticationAsync(token, NeedAuthorizeUrl); + var response = await client.GetAsync(NeedAuthorizeUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } @@ -58,12 +57,11 @@ namespace Timeline.Tests [Fact] public async Task AdminAuthorizationTest() { - using (var client = _factory.CreateDefaultClient()) + using (var client = await _factory.CreateClientWithUser("admin", "admin")) { - var token = (await client.CreateUserTokenAsync("admin", "admin")).Token; - var response1 = await client.SendWithAuthenticationAsync(token, BothUserAndAdminUrl); + var response1 = await client.GetAsync(BothUserAndAdminUrl); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); - var response2 = await client.SendWithAuthenticationAsync(token, OnlyAdminUrl); + var response2 = await client.GetAsync(OnlyAdminUrl); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); } } diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs new file mode 100644 index 00000000..40191009 --- /dev/null +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Entities; +using Xunit; + +namespace Timeline.Tests.Helpers.Authentication +{ + public static class AuthenticationExtensions + { + private const string CreateTokenUrl = "/token/create"; + + public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) + { + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + if (assertSuccess) + Assert.True(result.Success); + + return result; + } + + public static async Task CreateClientWithUser(this WebApplicationFactory factory, string username, string password) where T : class + { + var client = factory.CreateDefaultClient(); + var token = (await client.CreateUserTokenAsync(username, password)).Token; + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + return client; + } + + public static async Task SendWithAuthenticationAsync(this HttpClient client, string token, string path, Action requestBuilder = null) + { + var request = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, path), + }; + request.Headers.Add("Authorization", "Bearer " + token); + requestBuilder?.Invoke(request); + return await client.SendAsync(request); + } + } +} diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs deleted file mode 100644 index c0051c53..00000000 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Entities; -using Xunit; - -namespace Timeline.Tests.Helpers.Authentication -{ - public static class AuthenticationHttpClientExtensions - { - private const string CreateTokenUrl = "/User/CreateToken"; - - public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) - { - var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - if (assertSuccess) - Assert.True(result.Success); - - return result; - } - - public static async Task SendWithAuthenticationAsync(this HttpClient client, string token, string path, Action requestBuilder = null) - { - var request = new HttpRequestMessage - { - RequestUri = new Uri(client.BaseAddress, path), - }; - request.Headers.Add("Authorization", "Bearer " + token); - requestBuilder?.Invoke(request); - return await client.SendAsync(request); - } - } -} diff --git a/Timeline.Tests/Helpers/TestUsers.cs b/Timeline.Tests/Helpers/TestUsers.cs new file mode 100644 index 00000000..b7005d54 --- /dev/null +++ b/Timeline.Tests/Helpers/TestUsers.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; + +namespace Timeline.Tests.Helpers +{ + public static class TestMockUsers + { + static TestMockUsers() + { + var mockUsers = new List(); + var passwordService = new PasswordService(null); + + mockUsers.Add(new User + { + Name = "user", + EncryptedPassword = passwordService.HashPassword("user"), + RoleString = "user" + }); + mockUsers.Add(new User + { + Name = "admin", + EncryptedPassword = passwordService.HashPassword("admin"), + RoleString = "user,admin" + }); + + MockUsers = mockUsers; + + var mockUserInfos = mockUsers.Select(u => new UserInfo(u)).ToList(); + mockUserInfos.Sort(UserInfo.Comparer); + MockUserInfos = mockUserInfos; + } + + public static List MockUsers { get; } + + public static IReadOnlyList MockUserInfos { get; } + } +} diff --git a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs index 4a7f87fb..a34217f4 100644 --- a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs +++ b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Timeline.Models; -using Timeline.Services; using Xunit.Abstractions; namespace Timeline.Tests.Helpers @@ -42,28 +41,10 @@ namespace Timeline.Tests.Helpers var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService(); - var passwordService = new PasswordService(null); - // Ensure the database is created. db.Database.EnsureCreated(); - db.Users.AddRange(new User[] { - new User - { - Id = 0, - Name = "user", - EncryptedPassword = passwordService.HashPassword("user"), - RoleString = "user" - }, - new User - { - Id = 0, - Name = "admin", - EncryptedPassword = passwordService.HashPassword("admin"), - RoleString = "user,admin" - } - }); - + db.Users.AddRange(TestMockUsers.MockUsers); db.SaveChanges(); } }); diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index fa9c7628..39ffc928 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -12,8 +12,8 @@ namespace Timeline.Tests { public class JwtTokenUnitTest : IClassFixture> { - private const string CreateTokenUrl = "User/CreateToken"; - private const string VerifyTokenUrl = "User/VerifyToken"; + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; private readonly WebApplicationFactory _factory; diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 57e04fc0..820737cc 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -8,7 +8,7 @@ - + all diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs new file mode 100644 index 00000000..7d8cc824 --- /dev/null +++ b/Timeline.Tests/UserUnitTest.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Newtonsoft.Json; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class UserUnitTest : IClassFixture> + { + private readonly WebApplicationFactory _factory; + + public UserUnitTest(WebApplicationFactory factory, ITestOutputHelper outputHelper) + { + _factory = factory.WithTestConfig(outputHelper); + } + + [Fact] + public async Task UserTest() + { + using (var client = await _factory.CreateClientWithUser("admin", "admin")) + { + var res1 = await client.GetAsync("users"); + Assert.Equal(HttpStatusCode.OK, res1.StatusCode); + var users = JsonConvert.DeserializeObject(await res1.Content.ReadAsStringAsync()).ToList(); + users.Sort(UserInfo.Comparer); + Assert.Equal(TestMockUsers.MockUserInfos, users, UserInfo.EqualityComparer); + } + } + } +} -- cgit v1.2.3 From 748aa44ccaf88686ffbaf9e31d025be24e2d200a Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 21 Apr 2019 22:40:55 +0800 Subject: Fix a bug in test code. --- Timeline.Tests/Helpers/TestUsers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Timeline.Tests') diff --git a/Timeline.Tests/Helpers/TestUsers.cs b/Timeline.Tests/Helpers/TestUsers.cs index b7005d54..89ddf218 100644 --- a/Timeline.Tests/Helpers/TestUsers.cs +++ b/Timeline.Tests/Helpers/TestUsers.cs @@ -28,7 +28,7 @@ namespace Timeline.Tests.Helpers MockUsers = mockUsers; - var mockUserInfos = mockUsers.Select(u => new UserInfo(u)).ToList(); + var mockUserInfos = mockUsers.Select(u => UserInfo.Create(u)).ToList(); mockUserInfos.Sort(UserInfo.Comparer); MockUserInfos = mockUserInfos; } -- cgit v1.2.3 From a2d8695d1e46d271bab40ea192afffee65f7538f Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 22 Apr 2019 14:45:52 +0800 Subject: Move http models in to a new namespace. Revert last commit. --- .../Authentication/AuthenticationExtensions.cs | 2 +- Timeline.Tests/JwtTokenUnitTest.cs | 2 +- Timeline/Controllers/TokenController.cs | 2 +- Timeline/Controllers/UserController.cs | 46 ++++++---------------- Timeline/Entities/Common.cs | 12 ------ Timeline/Entities/Http/Common.cs | 29 ++++++++++++++ Timeline/Entities/Http/Token.cs | 26 ++++++++++++ Timeline/Entities/Http/User.cs | 26 ++++++++++++ Timeline/Entities/Token.cs | 26 ------------ Timeline/Entities/User.cs | 30 -------------- Timeline/Services/UserService.cs | 10 ++--- 11 files changed, 102 insertions(+), 109 deletions(-) delete mode 100644 Timeline/Entities/Common.cs create mode 100644 Timeline/Entities/Http/Common.cs create mode 100644 Timeline/Entities/Http/Token.cs create mode 100644 Timeline/Entities/Http/User.cs delete mode 100644 Timeline/Entities/Token.cs delete mode 100644 Timeline/Entities/User.cs (limited to 'Timeline.Tests') diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index 40191009..cda9fe99 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -4,7 +4,7 @@ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Entities; +using Timeline.Entities.Http; using Xunit; namespace Timeline.Tests.Helpers.Authentication diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index 39ffc928..8a503bd7 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using System.Net; using System.Net.Http; -using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Xunit; diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 463fb83c..0be5fb2f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Threading.Tasks; -using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Services; namespace Timeline.Controllers diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index d2708eeb..59c7a48c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Services; namespace Timeline.Controllers @@ -48,50 +49,29 @@ namespace Timeline.Controllers } } - [HttpPatch("user/{username}"), Authorize] + [HttpPatch("user/{username}"), Authorize(Roles = "admin")] public async Task Patch([FromBody] UserModifyRequest request, [FromRoute] string username) { - if (User.IsInRole("admin")) - { - var result = await _userService.PatchUser(username, request.Password, request.Roles); - switch (result) - { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(); - default: - throw new Exception("Unreachable code."); - } - } - else + var result = await _userService.PatchUser(username, request.Password, request.Roles); + switch (result) { - if (User.Identity.Name != username) - return StatusCode(403, new MessageResponse("Can't patch other user when you are not admin.")); - if (request.Roles != null) - return StatusCode(403, new MessageResponse("Can't patch roles when you are not admin.")); - - var result = await _userService.PatchUser(username, request.Password, null); - switch (result) - { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(new MessageResponse("This username no longer exists. Please update your token.")); - default: - throw new Exception("Unreachable code."); - } + case PatchUserResult.Success: + return Ok(); + case PatchUserResult.NotExists: + return NotFound(); + default: + throw new Exception("Unreachable code."); } } [HttpDelete("user/{username}"), Authorize(Roles = "admin")] - public async Task> Delete([FromRoute] string username) + public async Task Delete([FromRoute] string username) { var result = await _userService.DeleteUser(username); switch (result) { - case DeleteUserResult.Success: - return Ok(UserDeleteResponse.Success); + case DeleteUserResult.Deleted: + return Ok(UserDeleteResponse.Deleted); case DeleteUserResult.NotExists: return Ok(UserDeleteResponse.NotExists); default: diff --git a/Timeline/Entities/Common.cs b/Timeline/Entities/Common.cs deleted file mode 100644 index 235a2a20..00000000 --- a/Timeline/Entities/Common.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Timeline.Entities -{ - public class MessageResponse - { - public MessageResponse(string message) - { - Message = message; - } - - public string Message { get; set; } - } -} diff --git a/Timeline/Entities/Http/Common.cs b/Timeline/Entities/Http/Common.cs new file mode 100644 index 00000000..9575e6fa --- /dev/null +++ b/Timeline/Entities/Http/Common.cs @@ -0,0 +1,29 @@ +namespace Timeline.Entities.Http +{ + public class ReturnCodeMessageResponse + { + public ReturnCodeMessageResponse() + { + + } + + public ReturnCodeMessageResponse(int code) + { + ReturnCode = code; + } + + public ReturnCodeMessageResponse(string message) + { + Message = message; + } + + public ReturnCodeMessageResponse(int code, string message) + { + ReturnCode = code; + Message = message; + } + + public int? ReturnCode { get; set; } = null; + public string Message { get; set; } = null; + } +} diff --git a/Timeline/Entities/Http/Token.cs b/Timeline/Entities/Http/Token.cs new file mode 100644 index 00000000..45ee0fc5 --- /dev/null +++ b/Timeline/Entities/Http/Token.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities.Http +{ + public class CreateTokenRequest + { + public string Username { get; set; } + public string Password { get; set; } + } + + public class CreateTokenResponse + { + public bool Success { get; set; } + public string Token { get; set; } + public UserInfo UserInfo { get; set; } + } + + public class VerifyTokenRequest + { + public string Token { get; set; } + } + + public class VerifyTokenResponse + { + public bool IsValid { get; set; } + public UserInfo UserInfo { get; set; } + } +} diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs new file mode 100644 index 00000000..24952ac7 --- /dev/null +++ b/Timeline/Entities/Http/User.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities.Http +{ + public class UserModifyRequest + { + public string Password { get; set; } + public string[] Roles { get; set; } + } + + public static class UserPutResponse + { + public const int CreatedCode = 0; + public const int ModifiedCode = 1; + + public static ReturnCodeMessageResponse Created { get; } = new ReturnCodeMessageResponse(CreatedCode, "A new user is created."); + public static ReturnCodeMessageResponse Modified { get; } = new ReturnCodeMessageResponse(ModifiedCode, "A existing user is modified."); + } + + public static class UserDeleteResponse + { + public const int DeletedCode = 0; + public const int NotExistsCode = 1; + + public static ReturnCodeMessageResponse Deleted { get; } = new ReturnCodeMessageResponse(DeletedCode, "A existing user is deleted."); + public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "User with given name does not exists."); + } +} diff --git a/Timeline/Entities/Token.cs b/Timeline/Entities/Token.cs deleted file mode 100644 index 1b5a469d..00000000 --- a/Timeline/Entities/Token.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Timeline.Entities -{ - public class CreateTokenRequest - { - public string Username { get; set; } - public string Password { get; set; } - } - - public class CreateTokenResponse - { - public bool Success { get; set; } - public string Token { get; set; } - public UserInfo UserInfo { get; set; } - } - - public class VerifyTokenRequest - { - public string Token { get; set; } - } - - public class VerifyTokenResponse - { - public bool IsValid { get; set; } - public UserInfo UserInfo { get; set; } - } -} diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs deleted file mode 100644 index eb126165..00000000 --- a/Timeline/Entities/User.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Timeline.Entities -{ - public class UserModifyRequest - { - public string Password { get; set; } - public string[] Roles { get; set; } - } - - public class UserPutResponse - { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; - - public static UserPutResponse Created { get; } = new UserPutResponse { ReturnCode = CreatedCode }; - public static UserPutResponse Modified { get; } = new UserPutResponse { ReturnCode = ModifiedCode }; - - public int ReturnCode { get; set; } - } - - public class UserDeleteResponse - { - public const int SuccessCode = 0; - public const int NotExistsCode = 1; - - public static UserDeleteResponse Success { get; } = new UserDeleteResponse { ReturnCode = SuccessCode }; - public static UserDeleteResponse NotExists { get; } = new UserDeleteResponse { ReturnCode = NotExistsCode }; - - public int ReturnCode { get; set; } - } -} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index a0d358dd..8615d0c5 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -40,9 +40,9 @@ namespace Timeline.Services public enum DeleteUserResult { /// - /// Succeed to delete user. + /// A existing user is deleted. /// - Success, + Deleted, /// /// A user of given username does not exist. /// @@ -105,12 +105,12 @@ namespace Timeline.Services /// /// Delete a user of given username. - /// Return if success to delete. + /// Return if the user is deleted. /// Return if the user of given username /// does not exist. /// /// Username of thet user to delete. - /// if success to delete. + /// if the user is deleted. /// if the user doesn't exist. Task DeleteUser(string username); } @@ -250,7 +250,7 @@ namespace Timeline.Services _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); - return DeleteUserResult.Success; + return DeleteUserResult.Deleted; } } } -- cgit v1.2.3 From ec31ef8e58928ce16cee7a0a57322fd466667649 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.Tests') 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 From cbcd12e0a4b7e363e3e275b870ac3c20f1c96836 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 24 Apr 2019 00:22:25 +0800 Subject: Add ObjectExists implementation in cos. Remove Test host environment. --- .../Helpers/WebApplicationFactoryExtensions.cs | 1 - Timeline.Tests/QCloudCosServiceUnitTest.cs | 33 +++++++- Timeline/EnvironmentConstants.cs | 14 ---- Timeline/Services/TencentCloudCosService.cs | 88 +++++++++++++++++++--- Timeline/Startup.cs | 11 +-- Timeline/appsettings.Test.json | 14 ---- 6 files changed, 112 insertions(+), 49 deletions(-) delete mode 100644 Timeline/EnvironmentConstants.cs delete mode 100644 Timeline/appsettings.Test.json (limited to 'Timeline.Tests') diff --git a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs index a34217f4..a7616b41 100644 --- a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs +++ b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs @@ -15,7 +15,6 @@ namespace Timeline.Tests.Helpers return factory.WithWebHostBuilder(builder => { builder - .UseEnvironment(EnvironmentConstants.TestEnvironmentName) .ConfigureLogging(logging => { logging.AddXunit(outputHelper); diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs index c02f70be..b99352b9 100644 --- a/Timeline.Tests/QCloudCosServiceUnitTest.cs +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -1,12 +1,24 @@ -using System; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; +using System.Threading.Tasks; using Timeline.Services; +using Timeline.Tests.Helpers; using Xunit; +using Xunit.Abstractions; namespace Timeline.Tests { - public class QCloudCosServiceUnitTest + public class QCloudCosServiceUnitTest : IClassFixture> { + private readonly WebApplicationFactory _factory; + + public QCloudCosServiceUnitTest(WebApplicationFactory factory, ITestOutputHelper outputHelper) + { + _factory = factory.WithTestConfig(outputHelper); + } + [Fact] public void GenerateSignatureTest() { @@ -37,5 +49,22 @@ namespace Timeline.Tests 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)); } + + /* + [Fact] + public async Task ObjectExistsTest() + { + _factory.CreateDefaultClient().Dispose(); + + using (var serviceScope = _factory.Server.Host.Services.CreateScope()) + { + var services = serviceScope.ServiceProvider; + var service = services.GetRequiredService(); + Assert.True(await service.ObjectExists("avatar", "__default")); + Assert.False(await service.ObjectExists("avatar", "haha")); + Assert.False(await service.ObjectExists("haha", "haha")); + } + } + */ } } diff --git a/Timeline/EnvironmentConstants.cs b/Timeline/EnvironmentConstants.cs deleted file mode 100644 index 5ffc3623..00000000 --- a/Timeline/EnvironmentConstants.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Hosting; - -namespace Timeline -{ - public static class EnvironmentConstants - { - public const string TestEnvironmentName = "Test"; - - public static bool IsTest(this IHostingEnvironment environment) - { - return environment.EnvironmentName == TestEnvironmentName; - } - } -} diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index 1bfcf745..8dbd3614 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -5,6 +5,7 @@ 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.Threading.Tasks; @@ -20,13 +21,15 @@ namespace Timeline.Services public class QCloudCosService : IQCloudCosService { - private readonly QCloudCosConfig _config; + private readonly IOptionsMonitor _config; private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; - public QCloudCosService(IOptions config, ILogger logger) + public QCloudCosService(IOptionsMonitor config, ILogger logger, IHttpClientFactory httpClientFactory) { - _config = config.Value; + _config = config; _logger = logger; + _httpClientFactory = httpClientFactory; } public class QCloudCredentials @@ -45,6 +48,17 @@ namespace Timeline.Services 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; } } @@ -57,14 +71,15 @@ namespace Timeline.Services 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(); + 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; } @@ -103,7 +118,7 @@ namespace Timeline.Services } var httpString = new StringBuilder() - .Append(request.Method).Append('\n') + .Append(request.Method.ToLower()).Append('\n') .Append(request.Uri).Append('\n') .Append(Join(transformedParameters)).Append('\n') .Append(Join(transformedHeaders)).Append('\n') @@ -130,9 +145,64 @@ namespace Timeline.Services return Join(result); } - public Task ObjectExists(string bucket, string key) + private QCloudCredentials GetCredentials() { - throw new NotImplementedException(); + 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 ObjectExists(string bucket, string key) + { + if (bucket == null) + throw new ArgumentNullException(nameof(bucket)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + + var client = _httpClientFactory.CreateClient(); + + var host = GetHost(bucket); + + var request = new HttpRequestMessage(); + request.Method = HttpMethod.Head; + request.RequestUri = new Uri($"https://{host}/{key}"); + request.Headers.Host = host; + request.Headers.Date = DateTimeOffset.Now; + request.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo + { + Method = "head", + Uri = "/" + key, + 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 GetObjectUrl(string bucket, string key) diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 12d60843..46d0afe5 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -81,6 +81,8 @@ namespace Timeline }); }); + services.AddHttpClient(); + services.Configure(Configuration.GetSection(nameof(QCloudCosConfig))); services.AddSingleton(); } @@ -88,15 +90,6 @@ namespace Timeline // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { - if (Environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - } - app.UseCors(corsPolicyName); app.UseForwardedHeaders(new ForwardedHeadersOptions diff --git a/Timeline/appsettings.Test.json b/Timeline/appsettings.Test.json deleted file mode 100644 index ea32348b..00000000 --- a/Timeline/appsettings.Test.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "Microsoft.AspNetCore.Authentication": "Debug", - "Microsoft.AspNetCore.Authorization": "Debug" - } - }, - "JwtConfig": { - "SigningKey": "crupest hahahahahahahhahahahahaha" - } -} -- cgit v1.2.3 From 40a1e1d96940ae005db6467cd4207b25d6261837 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 25 Apr 2019 18:53:05 +0800 Subject: Implement generate object get url in cos. --- Timeline.Tests/QCloudCosServiceUnitTest.cs | 45 +++++++++++++++++++--- Timeline/Services/TencentCloudCosService.cs | 58 +++++++++++++++++++++++++---- Timeline/Services/UserService.cs | 6 +-- 3 files changed, 93 insertions(+), 16 deletions(-) (limited to 'Timeline.Tests') diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs index b99352b9..b0e6a868 100644 --- a/Timeline.Tests/QCloudCosServiceUnitTest.cs +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using Timeline.Services; using Timeline.Tests.Helpers; @@ -17,6 +18,19 @@ namespace Timeline.Tests public QCloudCosServiceUnitTest(WebApplicationFactory factory, ITestOutputHelper outputHelper) { _factory = factory.WithTestConfig(outputHelper); + _factory.CreateDefaultClient().Dispose(); // Ensure test server is created. + } + + [Fact] + public void ValidateBucketNameTest() + { + Assert.True(QCloudCosService.ValidateBucketName("hello")); + Assert.True(QCloudCosService.ValidateBucketName("hello0123")); + Assert.True(QCloudCosService.ValidateBucketName("hello0123-hello")); + Assert.False(QCloudCosService.ValidateBucketName("-hello")); + Assert.False(QCloudCosService.ValidateBucketName("hello-")); + Assert.False(QCloudCosService.ValidateBucketName("helloU")); + Assert.False(QCloudCosService.ValidateBucketName("hello!")); } [Fact] @@ -50,21 +64,40 @@ namespace Timeline.Tests 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)); } - /* +/* +// Tests in this part need secret configs in cos. +#region SecretTests [Fact] public async Task ObjectExistsTest() { - _factory.CreateDefaultClient().Dispose(); + using (var serviceScope = _factory.Server.Host.Services.CreateScope()) + { + var services = serviceScope.ServiceProvider; + var service = services.GetRequiredService(); + Assert.True(await service.IsObjectExists("avatar", "__default")); + Assert.False(await service.IsObjectExists("avatar", "haha")); + Assert.False(await service.IsObjectExists("haha", "haha")); + } + } + // 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() + { using (var serviceScope = _factory.Server.Host.Services.CreateScope()) { var services = serviceScope.ServiceProvider; var service = services.GetRequiredService(); - Assert.True(await service.ObjectExists("avatar", "__default")); - Assert.False(await service.ObjectExists("avatar", "haha")); - Assert.False(await service.ObjectExists("haha", "haha")); + var url = service.GenerateObjectGetUrl("avatar", "__default"); + using (var client = _factory.CreateClient()) + { + var res = await client.GetAsync(url); + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + } } } - */ +#endregion +*/ } } diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index 8dbd3614..f4358714 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -8,6 +8,7 @@ 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; @@ -15,8 +16,21 @@ namespace Timeline.Services { public interface IQCloudCosService { - Task ObjectExists(string bucket, string key); - string GetObjectUrl(string bucket, string key); + /// + /// 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 @@ -32,6 +46,13 @@ namespace Timeline.Services _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; } @@ -161,26 +182,29 @@ namespace Timeline.Services return $"{bucket}-{config.AppId}.cos.{config.Region}.myqcloud.com"; } - public async Task ObjectExists(string bucket, string key) + 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}/{key}"); + 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 = "/" + key, + Uri = "/" + encodedKey, Headers = new Dictionary { ["Host"] = host @@ -205,9 +229,29 @@ namespace Timeline.Services } } - public string GetObjectUrl(string bucket, string key) + public string GenerateObjectGetUrl(string bucket, string key) { - throw new NotImplementedException(); + 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/UserService.cs b/Timeline/Services/UserService.cs index d1555660..4a47ca0f 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -301,11 +301,11 @@ namespace Timeline.Services public async Task GetAvatarUrl(string username) { - var exists = await _cosService.ObjectExists("avatar", username); + var exists = await _cosService.IsObjectExists("avatar", username); if (exists) - return _cosService.GetObjectUrl("avatar", username); + return _cosService.GenerateObjectGetUrl("avatar", username); else - return _cosService.GetObjectUrl("avatar", "__default"); + return _cosService.GenerateObjectGetUrl("avatar", "__default"); } } } -- cgit v1.2.3 From 1affca9259fc3178595e4dfdf2389977110c8f74 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 (limited to 'Timeline.Tests') 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