From 87513987a23ecd75cd21015ed215bae3b279d8c5 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 19 Aug 2019 22:52:01 +0800 Subject: Add check for content in avatar put. --- Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 53 ++++++++++++++++++++++- Timeline/Controllers/UserAvatarController.cs | 26 +++++++++-- Timeline/Filters/ContentHeaderAttributes.cs | 48 ++++++++++++++++++++ Timeline/Models/Http/Common.cs | 19 ++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 Timeline/Filters/ContentHeaderAttributes.cs diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs index 794f251b..0bed9598 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs @@ -7,8 +7,10 @@ using System; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Timeline.Controllers; +using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; @@ -76,8 +78,28 @@ namespace Timeline.Tests.IntegratedTests } { - var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/notaccept"); - res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Missing_ContentLength); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1; + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Missing_ContentType); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 0; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Zero_ContentLength); } { @@ -85,6 +107,33 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); } + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1000 * 1000 * 11; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_TooBig); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 2; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less); + } + + { + var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + content.Headers.ContentLength = 1; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger); + } + { var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 89d2650c..ffadcb86 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; using Timeline.Authenticate; +using Timeline.Filters; using Timeline.Models.Http; using Timeline.Services; @@ -22,6 +23,9 @@ namespace Timeline.Controllers public const int Put_BadFormat_CantDecode = -2011; public const int Put_BadFormat_UnmatchedFormat = -2012; public const int Put_BadFormat_BadSize = -2013; + public const int Put_Content_TooBig = -2021; + public const int Put_Content_UnmatchedLength_Less = -2022; + public const int Put_Content_UnmatchedLength_Bigger = -2023; public const int Delete_UserNotExist = -3001; public const int Delete_Forbid = -3002; @@ -55,7 +59,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}/avatar")] [Authorize] - public async Task Get(string username) + public async Task Get([FromRoute] string username) { const string IfModifiedSinceHeaderKey = "If-Modified-Since"; try @@ -83,9 +87,15 @@ namespace Timeline.Controllers [HttpPut("users/{username}/avatar")] [Authorize] + [RequireContentType, RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put(string username) { + var contentLength = Request.ContentLength.Value; + if (contentLength > 1000 * 1000 * 10) + return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig, + "Content can't be bigger than 10MB.")); + if (!User.IsAdmin() && User.Identity.Name != username) { _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); @@ -95,8 +105,16 @@ namespace Timeline.Controllers try { - var data = new byte[Convert.ToInt32(Request.ContentLength)]; - await Request.Body.ReadAsync(data, 0, data.Length); + var data = new byte[contentLength]; + var bytesRead = await Request.Body.ReadAsync(data); + + if (bytesRead != contentLength) + return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Less, + $"Content length in header is {contentLength} but actual length is {bytesRead}.")); + + if (Request.Body.ReadByte() != -1) + return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Bigger, + $"Content length in header is {contentLength} but actual length is bigger than that.")); await _service.SetAvatar(username, new Avatar { @@ -121,7 +139,7 @@ namespace Timeline.Controllers [HttpDelete("users/{username}/avatar")] [Authorize] - public async Task Delete(string username) + public async Task Delete([FromRoute] string username) { if (!User.IsAdmin() && User.Identity.Name != username) { diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs new file mode 100644 index 00000000..14685a01 --- /dev/null +++ b/Timeline/Filters/ContentHeaderAttributes.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; + +namespace Timeline.Filters +{ + public class RequireContentTypeAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.ContentType == null) + { + context.Result = new BadRequestObjectResult(CommonResponse.MissingContentType()); + } + } + } + + public class RequireContentLengthAttribute : ActionFilterAttribute + { + public RequireContentLengthAttribute() + : this(true) + { + + } + + public RequireContentLengthAttribute(bool requireNonZero) + { + RequireNonZero = requireNonZero; + } + + public bool RequireNonZero { get; set; } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.ContentLength == null) + { + context.Result = new BadRequestObjectResult(CommonResponse.MissingContentLength()); + return; + } + + if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) + { + context.Result = new BadRequestObjectResult(CommonResponse.ZeroContentLength()); + return; + } + } + } +} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index b4932754..50f6836e 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -9,6 +9,10 @@ namespace Timeline.Models.Http /// For example a required field is null. /// public const int InvalidModel = -100; + + public const int Header_Missing_ContentType = -111; + public const int Header_Missing_ContentLength = -112; + public const int Header_Zero_ContentLength = -113; } public static CommonResponse InvalidModel(string message) @@ -16,6 +20,21 @@ namespace Timeline.Models.Http return new CommonResponse(ErrorCodes.InvalidModel, message); } + public static CommonResponse MissingContentType() + { + return new CommonResponse(ErrorCodes.Header_Missing_ContentType, "Header Content-Type is required."); + } + + public static CommonResponse MissingContentLength() + { + return new CommonResponse(ErrorCodes.Header_Missing_ContentLength, "Header Content-Length is missing or of bad format."); + } + + public static CommonResponse ZeroContentLength() + { + return new CommonResponse(ErrorCodes.Header_Zero_ContentLength, "Header Content-Length must not be 0."); + } + public CommonResponse() { -- cgit v1.2.3