aboutsummaryrefslogtreecommitdiff
path: root/Timeline
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline')
-rw-r--r--Timeline/Controllers/UserAvatarController.cs25
-rw-r--r--Timeline/Filters/Header.cs69
-rw-r--r--Timeline/Formatters/BytesInputFormatter.cs79
-rw-r--r--Timeline/Models/ByteData.cs33
-rw-r--r--Timeline/Startup.cs1
5 files changed, 162 insertions, 45 deletions
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs
index 52e87df2..32f63fc6 100644
--- a/Timeline/Controllers/UserAvatarController.cs
+++ b/Timeline/Controllers/UserAvatarController.cs
@@ -3,10 +3,12 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
+using System.IO;
using System.Threading.Tasks;
using Timeline.Auth;
using Timeline.Filters;
using Timeline.Helpers;
+using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
using Timeline.Services;
@@ -72,20 +74,17 @@ namespace Timeline.Controllers
/// Set avatar of a user. You have to be administrator to change other's.
/// </summary>
/// <param name="username">Username of the user to set avatar of.</param>
+ /// <param name="body">The avatar data.</param>
[HttpPut("users/{username}/avatar")]
[Authorize]
- [RequireContentType, RequireContentLength]
[Consumes("image/png", "image/jpeg", "image/gif", "image/webp")]
+ [MaxContentLength(1000 * 1000 * 10)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<IActionResult> Put([FromRoute][Username] string username)
+ public async Task<IActionResult> Put([FromRoute][Username] string username, [FromBody] ByteData body)
{
- var contentLength = Request.ContentLength!.Value;
- if (contentLength > 1000 * 1000 * 10)
- return BadRequest(ErrorResponse.Common.Content.TooBig("10MB"));
-
if (!User.IsAdministrator() && User.Identity.Name != username)
{
_logger.LogInformation(Log.Format(LogPutForbid,
@@ -106,20 +105,10 @@ namespace Timeline.Controllers
try
{
- var data = new byte[contentLength];
- var bytesRead = await Request.Body.ReadAsync(data);
-
- if (bytesRead != contentLength)
- return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Smaller());
-
- var extraByte = new byte[1];
- if (await Request.Body.ReadAsync(extraByte) != 0)
- return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Bigger());
-
await _service.SetAvatar(id, new Avatar
{
- Data = data,
- Type = Request.ContentType
+ Data = body.Data,
+ Type = body.ContentType
});
_logger.LogInformation(Log.Format(LogPutSuccess,
diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs
index 0db11faf..cc5ddd9f 100644
--- a/Timeline/Filters/Header.cs
+++ b/Timeline/Filters/Header.cs
@@ -4,45 +4,60 @@ using Timeline.Models.Http;
namespace Timeline.Filters
{
- public class RequireContentTypeAttribute : ActionFilterAttribute
+ /// <summary>
+ /// Restrict max content length.
+ /// </summary>
+ public class MaxContentLengthFilter : IResourceFilter
{
- public override void OnActionExecuting(ActionExecutingContext context)
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="maxByteLength">Max length.</param>
+ public MaxContentLengthFilter(long maxByteLength)
{
- if (context.HttpContext.Request.ContentType == null)
- {
- context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentType_Missing());
- }
+ MaxByteLength = maxByteLength;
}
- }
-
- public class RequireContentLengthAttribute : ActionFilterAttribute
- {
- public RequireContentLengthAttribute()
- : this(true)
- {
- }
+ /// <summary>
+ /// Max length.
+ /// </summary>
+ public long MaxByteLength { get; set; }
- public RequireContentLengthAttribute(bool requireNonZero)
+ /// <inheritdoc/>
+ public void OnResourceExecuted(ResourceExecutedContext context)
{
- RequireNonZero = requireNonZero;
}
- public bool RequireNonZero { get; set; }
-
- public override void OnActionExecuting(ActionExecutingContext context)
+ /// <inheritdoc/>
+ public void OnResourceExecuting(ResourceExecutingContext context)
{
- if (context.HttpContext.Request.ContentLength == null)
+ var contentLength = context.HttpContext.Request.ContentLength;
+ if (contentLength != null && contentLength > MaxByteLength)
{
- context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentLength_Missing());
- return;
+ context.Result = new BadRequestObjectResult(ErrorResponse.Common.Content.TooBig(MaxByteLength + "B"));
}
+ }
+ }
- if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0)
- {
- context.Result = new BadRequestObjectResult(ErrorResponse.Common.Header.ContentLength_Zero());
- return;
- }
+ /// <summary>
+ /// Restrict max content length.
+ /// </summary>
+ public class MaxContentLengthAttribute : TypeFilterAttribute
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="maxByteLength">Max length.</param>
+ public MaxContentLengthAttribute(long maxByteLength)
+ : base(typeof(MaxContentLengthFilter))
+ {
+ MaxByteLength = maxByteLength;
+ Arguments = new object[] { maxByteLength };
}
+
+ /// <summary>
+ /// Max length.
+ /// </summary>
+ public long MaxByteLength { get; }
}
}
diff --git a/Timeline/Formatters/BytesInputFormatter.cs b/Timeline/Formatters/BytesInputFormatter.cs
new file mode 100644
index 00000000..ac6537c9
--- /dev/null
+++ b/Timeline/Formatters/BytesInputFormatter.cs
@@ -0,0 +1,79 @@
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+using Timeline.Models;
+
+namespace Timeline.Formatters
+{
+ /// <summary>
+ /// Formatter that reads body as bytes.
+ /// </summary>
+ public class BytesInputFormatter : InputFormatter
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ public BytesInputFormatter()
+ {
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/gif"));
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/webp"));
+ }
+
+ /// <inheritdoc/>
+ public override bool CanRead(InputFormatterContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ if (context.ModelType == typeof(ByteData))
+ return true;
+
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
+ {
+ var request = context.HttpContext.Request;
+ var contentLength = request.ContentLength;
+
+ var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<BytesInputFormatter>>();
+
+ if (contentLength == null)
+ {
+ logger.LogInformation("Failed to read body as bytes. Content-Length is not set.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ if (contentLength == 0)
+ {
+ logger.LogInformation("Failed to read body as bytes. Content-Length is 0.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ var bodyStream = request.Body;
+
+ var data = new byte[contentLength.Value];
+ var bytesRead = await bodyStream.ReadAsync(data);
+
+ if (bytesRead != contentLength)
+ {
+ logger.LogInformation("Failed to read body as bytes. Actual length of body is smaller than Content-Length.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ var extraByte = new byte[1];
+ if (await bodyStream.ReadAsync(extraByte) != 0)
+ {
+ logger.LogInformation("Failed to read body as bytes. Actual length of body is greater than Content-Length.");
+ return await InputFormatterResult.FailureAsync();
+ }
+
+ return await InputFormatterResult.SuccessAsync(new ByteData(data, request.ContentType));
+ }
+ }
+}
diff --git a/Timeline/Models/ByteData.cs b/Timeline/Models/ByteData.cs
new file mode 100644
index 00000000..7b832eb5
--- /dev/null
+++ b/Timeline/Models/ByteData.cs
@@ -0,0 +1,33 @@
+using NSwag.Annotations;
+
+namespace Timeline.Models
+{
+ /// <summary>
+ /// Model for reading http body as bytes.
+ /// </summary>
+ [OpenApiFile]
+ public class ByteData
+ {
+ /// <summary>
+ /// </summary>
+ /// <param name="data">The data.</param>
+ /// <param name="contentType">The content type.</param>
+ public ByteData(byte[] data, string contentType)
+ {
+ Data = data;
+ ContentType = contentType;
+ }
+
+ /// <summary>
+ /// Data.
+ /// </summary>
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+
+ /// <summary>
+ /// Content type.
+ /// </summary>
+ public string ContentType { get; }
+ }
+}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs
index 960bbc2c..408057e5 100644
--- a/Timeline/Startup.cs
+++ b/Timeline/Startup.cs
@@ -49,6 +49,7 @@ namespace Timeline
services.AddControllers(setup =>
{
setup.InputFormatters.Add(new StringInputFormatter());
+ setup.InputFormatters.Add(new BytesInputFormatter());
setup.UseApiRoutePrefix("api");
})
.AddJsonOptions(options =>