Skip to content

Commit afba012

Browse files
committed
Move RequestSizeLimit processing to EndpointRoutingMiddleware
1 parent 31df643 commit afba012

File tree

8 files changed

+314
-242
lines changed

8 files changed

+314
-242
lines changed

src/Http/Routing/src/EndpointRoutingMiddleware.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
67
using System.Runtime.CompilerServices;
78
using Microsoft.AspNetCore.Antiforgery;
89
using Microsoft.AspNetCore.Authorization;
910
using Microsoft.AspNetCore.Cors.Infrastructure;
1011
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Http.Features;
1113
using Microsoft.AspNetCore.Http.Metadata;
1214
using Microsoft.AspNetCore.Routing.Matching;
1315
using Microsoft.AspNetCore.Routing.ShortCircuit;
@@ -130,6 +132,15 @@ private Task SetRoutingAndContinue(HttpContext httpContext)
130132
_metrics.MatchSuccess(route, isFallback);
131133
}
132134

135+
// Map RequestSizeLimitMetadata to IHttpMaxRequestBodySizeFeature if present on the endpoint.
136+
// We do this during endpoint routing to ensure that successive middlewares in the pipeline
137+
// can access the feature with the correct value.
138+
SetMaxRequestBodySize(httpContext);
139+
if (httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>() is { } httpMaxRequestBodySizeFeature)
140+
{
141+
httpContext.Request.Body = new SizeLimitedStream(httpContext.Request.Body, httpMaxRequestBodySizeFeature.MaxRequestBodySize);
142+
}
143+
133144
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
134145
if (shortCircuitMetadata is not null)
135146
{
@@ -292,6 +303,41 @@ private static void ThrowCannotShortCircuitAnAntiforgeryRouteException(Endpoint
292303
"but this endpoint is marked with short circuit and it will execute on Routing Middleware.");
293304
}
294305

306+
private void SetMaxRequestBodySize(HttpContext context)
307+
{
308+
var sizeLimitMetadata = context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>();
309+
if (sizeLimitMetadata == null)
310+
{
311+
Log.MetadataNotFound(_logger);
312+
return;
313+
}
314+
315+
var maxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
316+
if (maxRequestBodySizeFeature == null)
317+
{
318+
Log.FeatureNotFound(_logger);
319+
}
320+
else if (maxRequestBodySizeFeature.IsReadOnly)
321+
{
322+
Log.FeatureIsReadOnly(_logger);
323+
}
324+
else
325+
{
326+
var maxRequestBodySize = sizeLimitMetadata.MaxRequestBodySize;
327+
maxRequestBodySizeFeature.MaxRequestBodySize = maxRequestBodySize;
328+
329+
if (maxRequestBodySize.HasValue)
330+
{
331+
Log.MaxRequestBodySizeSet(_logger,
332+
maxRequestBodySize.Value.ToString(CultureInfo.InvariantCulture));
333+
}
334+
else
335+
{
336+
Log.MaxRequestBodySizeDisabled(_logger);
337+
}
338+
}
339+
}
340+
295341
private static partial class Log
296342
{
297343
public static void MatchSuccess(ILogger logger, Endpoint endpoint)
@@ -320,5 +366,20 @@ public static void MatchSkipped(ILogger logger, Endpoint endpoint)
320366

321367
[LoggerMessage(7, LogLevel.Debug, "Matched endpoint '{EndpointName}' is a fallback endpoint.", EventName = "FallbackMatch")]
322368
public static partial void FallbackMatch(ILogger logger, Endpoint endpointName);
369+
370+
[LoggerMessage(8, LogLevel.Debug, $"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.", EventName = "MetadataNotFound")]
371+
public static partial void MetadataNotFound(ILogger logger);
372+
373+
[LoggerMessage(9, LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.", EventName = "FeatureNotFound")]
374+
public static partial void FeatureNotFound(ILogger logger);
375+
376+
[LoggerMessage(10, LogLevel.Warning, $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.", EventName = "FeatureIsReadOnly")]
377+
public static partial void FeatureIsReadOnly(ILogger logger);
378+
379+
[LoggerMessage(11, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")]
380+
public static partial void MaxRequestBodySizeSet(ILogger logger, string requestSize);
381+
382+
[LoggerMessage(12, LogLevel.Debug, "The maximum request body size has been disabled.", EventName = "MaxRequestBodySizeDisabled")]
383+
public static partial void MaxRequestBodySizeDisabled(ILogger logger);
323384
}
324385
}

src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
<Compile Include="$(SharedSourceRoot)RoslynUtils\TypeHelper.cs" />
3030
<Compile Include="$(SharedSourceRoot)MediaType\ReadOnlyMediaTypeHeaderValue.cs" LinkBase="Shared" />
3131
<Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" />
32-
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared"/>
33-
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared"/>
32+
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared" />
33+
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared" />
3434
<Compile Include="$(SharedSourceRoot)RouteValueDictionaryTrimmerWarning.cs" LinkBase="Shared" />
3535
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />
3636
<Compile Include="$(SharedSourceRoot)HttpParseResult.cs" LinkBase="Shared" />
3737
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
3838
<Compile Include="$(SharedSourceRoot)AntiforgeryMetadata.cs" LinkBase="Shared" />
3939
<Compile Include="$(SharedSourceRoot)HttpMethodExtensions.cs" LinkBase="Shared" />
40+
<Compile Include="$(SharedSourceRoot)SizeLimitedStream.cs" LinkBase="Shared" />
4041
</ItemGroup>
4142

4243
<ItemGroup>

src/Http/Routing/test/FunctionalTests/AntiforgeryTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,71 @@ public async Task MapRequestDelegate_WithForm_RequiresValidation_InvalidToken_Fa
381381
Assert.Equal("This form is being accessed with an invalid anti-forgery token. Validate the `IAntiforgeryValidationFeature` on the request before reading from the form.", exception.Message);
382382
}
383383

384+
[Theory]
385+
[InlineData(true)]
386+
[InlineData(false)]
387+
public async Task MapPost_WithForm_ValidToken_RequestSizeLimit_Works(bool hasLimit)
388+
{
389+
using var host = new HostBuilder()
390+
.ConfigureWebHost(webHostBuilder =>
391+
{
392+
webHostBuilder
393+
.Configure(app =>
394+
{
395+
app.Use((context, next) =>
396+
{
397+
context.Features.Set<IHttpMaxRequestBodySizeFeature>(new FakeHttpMaxRequestBodySizeFeature(5_000_000));
398+
return next(context);
399+
});
400+
app.UseRouting();
401+
app.UseAntiforgery();
402+
app.UseEndpoints(b =>
403+
b.MapPost("/todo", ([FromForm] Todo todo) => todo).WithMetadata(new RequestSizeLimitMetadata(hasLimit ? 2 : null)));
404+
})
405+
.UseTestServer();
406+
})
407+
.ConfigureServices(services =>
408+
{
409+
services.AddRouting();
410+
services.AddAntiforgery();
411+
})
412+
.Build();
413+
414+
using var server = host.GetTestServer();
415+
await host.StartAsync();
416+
var client = server.CreateClient();
417+
418+
var antiforgery = host.Services.GetRequiredService<IAntiforgery>();
419+
var antiforgeryOptions = host.Services.GetRequiredService<IOptions<AntiforgeryOptions>>();
420+
var tokens = antiforgery.GetAndStoreTokens(new DefaultHttpContext());
421+
var request = new HttpRequestMessage(HttpMethod.Post, "todo");
422+
request.Headers.Add("Cookie", antiforgeryOptions.Value.Cookie.Name + "=" + tokens.CookieToken);
423+
var nameValueCollection = new List<KeyValuePair<string, string>>
424+
{
425+
new KeyValuePair<string,string>("__RequestVerificationToken", tokens.RequestToken),
426+
new KeyValuePair<string,string>("name", "Test task"),
427+
new KeyValuePair<string,string>("isComplete", "false"),
428+
new KeyValuePair<string,string>("dueDate", DateTime.Today.AddDays(1).ToString()),
429+
};
430+
request.Content = new FormUrlEncodedContent(nameValueCollection);
431+
432+
if (hasLimit)
433+
{
434+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await client.SendAsync(request));
435+
Assert.Equal("The maximum number of bytes have been read.", exception.Message);
436+
}
437+
else
438+
{
439+
var response = await client.SendAsync(request);
440+
response.EnsureSuccessStatusCode();
441+
var body = await response.Content.ReadAsStringAsync();
442+
var result = JsonSerializer.Deserialize<Todo>(body, SerializerOptions);
443+
Assert.Equal("Test task", result.Name);
444+
Assert.False(result.IsCompleted);
445+
Assert.Equal(DateTime.Today.AddDays(1), result.DueDate);
446+
}
447+
}
448+
384449
class Todo
385450
{
386451
public string Name { get; set; }
@@ -393,4 +458,25 @@ class FromFormAttribute(string name = "") : Attribute, IFromFormMetadata
393458
{
394459
public string Name => name;
395460
}
461+
462+
class RequestSizeLimitMetadata(long? maxRequestBodySize) : IRequestSizeLimitMetadata
463+
{
464+
465+
public long? MaxRequestBodySize => maxRequestBodySize;
466+
}
467+
468+
private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
469+
{
470+
public FakeHttpMaxRequestBodySizeFeature(
471+
long? maxRequestBodySize = null,
472+
bool isReadOnly = false)
473+
{
474+
MaxRequestBodySize = maxRequestBodySize;
475+
IsReadOnly = isReadOnly;
476+
}
477+
478+
public bool IsReadOnly { get; }
479+
480+
public long? MaxRequestBodySize { get; set; }
481+
}
396482
}

src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Globalization;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.AspNetCore.Http.Features;
9+
using Microsoft.AspNetCore.Http.Metadata;
810
using Microsoft.AspNetCore.Routing.Matching;
911
using Microsoft.AspNetCore.Routing.TestObjects;
1012
using Microsoft.AspNetCore.Testing;
@@ -81,7 +83,7 @@ public async Task Invoke_OnCall_WritesToConfiguredLogger()
8183

8284
// Assert
8385
Assert.Empty(sink.Scopes);
84-
var write = Assert.Single(sink.Writes);
86+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "MatchSuccess"));
8587
Assert.Equal(expectedMessage, write.State?.ToString());
8688
Assert.True(eventFired);
8789
}
@@ -262,6 +264,162 @@ public async Task Invoke_CheckForFallbackMetadata_LogIfPresent(bool hasFallbackM
262264
}
263265
}
264266

267+
[Fact]
268+
public async Task Endpoint_BodySizeFeatureIsReadOnly()
269+
{
270+
// Arrange
271+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
272+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
273+
var logger = new Logger<EndpointRoutingMiddleware>(loggerFactory);
274+
275+
var httpContext = CreateHttpContext();
276+
var expectedRequestSizeLimit = 50;
277+
var maxRequestBodySizeFeature = new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit, true);
278+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(maxRequestBodySizeFeature);
279+
280+
var metadata = new List<object> { new RequestSizeLimitMetadata(100) };
281+
var middleware = CreateMiddleware(
282+
logger: logger,
283+
matcherFactory: new TestMatcherFactory(isHandled: true, setEndpointCallback: c =>
284+
{
285+
c.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(metadata), "myapp"));
286+
}));
287+
288+
// Act
289+
await middleware.Invoke(httpContext);
290+
291+
// Assert
292+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "FeatureIsReadOnly"));
293+
Assert.Equal($"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.", write.Message);
294+
295+
var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
296+
Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
297+
}
298+
299+
[Fact]
300+
public async Task Endpoint_DoesNotHaveBodySizeFeature()
301+
{
302+
// Arrange
303+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
304+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
305+
var logger = new Logger<EndpointRoutingMiddleware>(loggerFactory);
306+
307+
var httpContext = CreateHttpContext();
308+
309+
var metadata = new List<object> { new RequestSizeLimitMetadata(100) };
310+
var middleware = CreateMiddleware(
311+
logger: logger,
312+
matcherFactory: new TestMatcherFactory(isHandled: true, setEndpointCallback: c =>
313+
{
314+
c.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(metadata), "myapp"));
315+
}));
316+
317+
// Act
318+
await middleware.Invoke(httpContext);
319+
320+
// Assert
321+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "FeatureNotFound"));
322+
Assert.Equal($"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.", write.Message);
323+
}
324+
325+
[Fact]
326+
public async Task Endpoint_DoesNotHaveSizeLimitMetadata()
327+
{
328+
// Arrange
329+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
330+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
331+
var logger = new Logger<EndpointRoutingMiddleware>(loggerFactory);
332+
333+
var httpContext = CreateHttpContext();
334+
var expectedRequestSizeLimit = 50;
335+
var maxRequestBodySizeFeature = new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit, true);
336+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(maxRequestBodySizeFeature);
337+
338+
var middleware = CreateMiddleware(
339+
logger: logger,
340+
matcherFactory: new TestMatcherFactory(isHandled: true, setEndpointCallback: c =>
341+
{
342+
c.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp"));
343+
}));
344+
345+
// Act
346+
await middleware.Invoke(httpContext);
347+
348+
// Assert
349+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "MetadataNotFound"));
350+
Assert.Equal($"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.", write.Message);
351+
}
352+
353+
[Theory]
354+
[InlineData(true)]
355+
[InlineData(false)]
356+
public async Task Endpoint_HasBodySizeFeature_SetUsingSizeLimitMetadata(bool isRequestSizeLimitDisabled)
357+
{
358+
// Arrange
359+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
360+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
361+
var logger = new Logger<EndpointRoutingMiddleware>(loggerFactory);
362+
363+
var httpContext = CreateHttpContext();
364+
long? expectedRequestSizeLimit = isRequestSizeLimitDisabled ? null : 500L;
365+
var maxRequestBodySizeFeature = new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit, false);
366+
httpContext.Features.Set<IHttpMaxRequestBodySizeFeature>(maxRequestBodySizeFeature);
367+
var metadata = new RequestSizeLimitMetadata(expectedRequestSizeLimit);
368+
369+
var middleware = CreateMiddleware(
370+
logger: logger,
371+
matcherFactory: new TestMatcherFactory(isHandled: true, setEndpointCallback: c =>
372+
{
373+
c.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(metadata), "myapp"));
374+
}));
375+
376+
// Act
377+
await middleware.Invoke(httpContext);
378+
379+
// Assert
380+
381+
if (isRequestSizeLimitDisabled)
382+
{
383+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "MaxRequestBodySizeDisabled"));
384+
Assert.Equal("The maximum request body size has been disabled.", write.Message);
385+
}
386+
else
387+
{
388+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "MaxRequestBodySizeSet"));
389+
Assert.Equal($"The maximum request body size has been set to {expectedRequestSizeLimit}.", write.Message);
390+
}
391+
392+
var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
393+
Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
394+
}
395+
396+
private class RequestSizeLimitMetadata(long? maxRequestBodySize) : IRequestSizeLimitMetadata
397+
{
398+
399+
public long? MaxRequestBodySize => maxRequestBodySize;
400+
}
401+
402+
private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
403+
{
404+
public FakeHttpMaxRequestBodySizeFeature(
405+
long? maxRequestBodySize = null,
406+
bool isReadOnly = false)
407+
{
408+
MaxRequestBodySize = maxRequestBodySize;
409+
IsReadOnly = isReadOnly;
410+
}
411+
412+
public bool IsReadOnly { get; }
413+
414+
public long? MaxRequestBodySize { get; set; }
415+
}
416+
417+
private static void AssertLog(WriteContext log, LogLevel level, string message)
418+
{
419+
Assert.Equal(level, log.LogLevel);
420+
Assert.Equal(message, log.State.ToString());
421+
}
422+
265423
private HttpContext CreateHttpContext()
266424
{
267425
var httpContext = new DefaultHttpContext

0 commit comments

Comments
 (0)