Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 3e69df8

Browse files
committed
Merge branch 'release' into dev
2 parents 391db10 + 13f73c6 commit 3e69df8

19 files changed

+890
-231
lines changed

src/Microsoft.AspNetCore.Http/Features/FormFeature.cs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ namespace Microsoft.AspNetCore.Http.Features
1414
{
1515
public class FormFeature : IFormFeature
1616
{
17+
private static readonly FormOptions DefaultFormOptions = new FormOptions();
18+
1719
private readonly HttpRequest _request;
20+
private readonly FormOptions _options;
1821
private Task<IFormCollection> _parsedFormTask;
1922
private IFormCollection _form;
2023

@@ -27,15 +30,24 @@ public FormFeature(IFormCollection form)
2730

2831
Form = form;
2932
}
30-
3133
public FormFeature(HttpRequest request)
34+
: this(request, DefaultFormOptions)
35+
{
36+
}
37+
38+
public FormFeature(HttpRequest request, FormOptions options)
3239
{
3340
if (request == null)
3441
{
3542
throw new ArgumentNullException(nameof(request));
3643
}
44+
if (options == null)
45+
{
46+
throw new ArgumentNullException(nameof(options));
47+
}
3748

3849
_request = request;
50+
_options = options;
3951
}
4052

4153
private MediaTypeHeaderValue ContentType
@@ -118,6 +130,11 @@ private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancell
118130

119131
cancellationToken.ThrowIfCancellationRequested();
120132

133+
if (_options.BufferBody)
134+
{
135+
_request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit);
136+
}
137+
121138
FormCollection formFields = null;
122139
FormFileCollection files = null;
123140

@@ -129,14 +146,27 @@ private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancell
129146
if (HasApplicationFormContentType(contentType))
130147
{
131148
var encoding = FilterEncoding(contentType.Encoding);
132-
formFields = new FormCollection(await FormReader.ReadFormAsync(_request.Body, encoding, cancellationToken));
149+
using (var formReader = new FormReader(_request.Body, encoding)
150+
{
151+
KeyCountLimit = _options.KeyCountLimit,
152+
KeyLengthLimit = _options.KeyLengthLimit,
153+
ValueLengthLimit = _options.ValueLengthLimit,
154+
})
155+
{
156+
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
157+
}
133158
}
134159
else if (HasMultipartFormContentType(contentType))
135160
{
136161
var formAccumulator = new KeyValueAccumulator();
137162

138-
var boundary = GetBoundary(contentType);
139-
var multipartReader = new MultipartReader(boundary, _request.Body);
163+
var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit);
164+
var multipartReader = new MultipartReader(boundary, _request.Body)
165+
{
166+
HeadersCountLimit = _options.MultipartHeadersCountLimit,
167+
HeadersLengthLimit = _options.MultipartHeadersLengthLimit,
168+
BodyLengthLimit = _options.MultipartBodyLengthLimit,
169+
};
140170
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
141171
while (section != null)
142172
{
@@ -145,7 +175,8 @@ private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancell
145175
if (HasFileContentDisposition(contentDisposition))
146176
{
147177
// Enable buffering for the file if not already done for the full body
148-
section.EnableRewind(_request.HttpContext.Response.RegisterForDispose);
178+
section.EnableRewind(_request.HttpContext.Response.RegisterForDispose,
179+
_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);
149180
// Find the end
150181
await section.Body.DrainAsync(cancellationToken);
151182

@@ -169,6 +200,10 @@ private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancell
169200
{
170201
files = new FormFileCollection();
171202
}
203+
if (files.Count >= _options.KeyCountLimit)
204+
{
205+
throw new InvalidDataException($"Form key count limit {_options.KeyCountLimit} exceeded.");
206+
}
172207
files.Add(file);
173208
}
174209
else if (HasFormDataContentDisposition(contentDisposition))
@@ -177,14 +212,20 @@ private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancell
177212
//
178213
// value
179214

215+
// Do not limit the key name length here because the mulipart headers length limit is already in effect.
180216
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
181217
MediaTypeHeaderValue mediaType;
182218
MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
183219
var encoding = FilterEncoding(mediaType?.Encoding);
184220
using (var reader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
185221
{
222+
// The value length limit is enforced by MultipartBodyLengthLimit
186223
var value = await reader.ReadToEndAsync();
187224
formAccumulator.Append(key, value);
225+
if (formAccumulator.Count > _options.KeyCountLimit)
226+
{
227+
throw new InvalidDataException($"Form key count limit {_options.KeyCountLimit} exceeded.");
228+
}
188229
}
189230
}
190231
else
@@ -261,13 +302,17 @@ private bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisp
261302
}
262303

263304
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
264-
// TODO: Limit the length of boundary we accept. The spec says ~70 chars.
265-
private static string GetBoundary(MediaTypeHeaderValue contentType)
305+
// The spec says 70 characters is a reasonable limit.
306+
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
266307
{
267308
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
268309
if (string.IsNullOrWhiteSpace(boundary))
269310
{
270-
throw new InvalidOperationException("Missing content-type boundary.");
311+
throw new InvalidDataException("Missing content-type boundary.");
312+
}
313+
if (boundary.Length > lengthLimit)
314+
{
315+
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
271316
}
272317
return boundary;
273318
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.WebUtilities;
5+
6+
namespace Microsoft.AspNetCore.Http.Features
7+
{
8+
public class FormOptions
9+
{
10+
public const int DefaultMemoryBufferThreshold = 1024 * 64;
11+
public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128;
12+
public const int DefaultMultipartBoundaryLengthLimit = 128;
13+
public const long DefaultMultipartBodyLengthLimit = 1024 * 1024 * 128;
14+
15+
public bool BufferBody { get; set; } = false;
16+
public int MemoryBufferThreshold { get; set; } = DefaultMemoryBufferThreshold;
17+
public long BufferBodyLengthLimit { get; set; } = DefaultBufferBodyLengthLimit;
18+
public int KeyCountLimit { get; set; } = FormReader.DefaultKeyCountLimit;
19+
public int KeyLengthLimit { get; set; } = FormReader.DefaultKeyLengthLimit;
20+
public int ValueLengthLimit { get; set; } = FormReader.DefaultValueLengthLimit;
21+
public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit;
22+
public int MultipartHeadersCountLimit { get; set; } = MultipartReader.DefaultHeadersCountLimit;
23+
public int MultipartHeadersLengthLimit { get; set; } = MultipartReader.DefaultHeadersLengthLimit;
24+
public long MultipartBodyLengthLimit { get; set; } = DefaultMultipartBodyLengthLimit;
25+
}
26+
}

src/Microsoft.AspNetCore.Http/HttpContextFactory.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,34 @@
55
using System.Text;
66
using Microsoft.AspNetCore.Http.Features;
77
using Microsoft.Extensions.ObjectPool;
8+
using Microsoft.Extensions.Options;
89

910
namespace Microsoft.AspNetCore.Http
1011
{
1112
public class HttpContextFactory : IHttpContextFactory
1213
{
1314
private readonly ObjectPool<StringBuilder> _builderPool;
1415
private readonly IHttpContextAccessor _httpContextAccessor;
16+
private readonly FormOptions _formOptions;
1517

16-
public HttpContextFactory(ObjectPoolProvider poolProvider)
17-
: this(poolProvider, httpContextAccessor: null)
18+
public HttpContextFactory(ObjectPoolProvider poolProvider, IOptions<FormOptions> formOptions)
19+
: this(poolProvider, formOptions, httpContextAccessor: null)
1820
{
1921
}
2022

21-
public HttpContextFactory(ObjectPoolProvider poolProvider, IHttpContextAccessor httpContextAccessor)
23+
public HttpContextFactory(ObjectPoolProvider poolProvider, IOptions<FormOptions> formOptions, IHttpContextAccessor httpContextAccessor)
2224
{
2325
if (poolProvider == null)
2426
{
2527
throw new ArgumentNullException(nameof(poolProvider));
2628
}
29+
if (formOptions == null)
30+
{
31+
throw new ArgumentNullException(nameof(formOptions));
32+
}
2733

2834
_builderPool = poolProvider.CreateStringBuilderPool();
35+
_formOptions = formOptions.Value;
2936
_httpContextAccessor = httpContextAccessor;
3037
}
3138

@@ -45,6 +52,9 @@ public HttpContext Create(IFeatureCollection featureCollection)
4552
_httpContextAccessor.HttpContext = httpContext;
4653
}
4754

55+
var formFeature = new FormFeature(httpContext.Request, _formOptions);
56+
featureCollection.Set<IFormFeature>(formFeature);
57+
4858
return httpContext;
4959
}
5060

src/Microsoft.AspNetCore.Http/Internal/BufferingHelper.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static string TempDirectory
3838
}
3939
}
4040

41-
public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold)
41+
public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
4242
{
4343
if (request == null)
4444
{
@@ -48,14 +48,15 @@ public static HttpRequest EnableRewind(this HttpRequest request, int bufferThres
4848
var body = request.Body;
4949
if (!body.CanSeek)
5050
{
51-
var fileStream = new FileBufferingReadStream(body, bufferThreshold, _getTempDirectory);
51+
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
5252
request.Body = fileStream;
5353
request.HttpContext.Response.RegisterForDispose(fileStream);
5454
}
5555
return request;
5656
}
5757

58-
public static MultipartSection EnableRewind(this MultipartSection section, Action<IDisposable> registerForDispose, int bufferThreshold = DefaultBufferThreshold)
58+
public static MultipartSection EnableRewind(this MultipartSection section, Action<IDisposable> registerForDispose,
59+
int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
5960
{
6061
if (section == null)
6162
{
@@ -69,7 +70,7 @@ public static MultipartSection EnableRewind(this MultipartSection section, Actio
6970
var body = section.Body;
7071
if (!body.CanSeek)
7172
{
72-
var fileStream = new FileBufferingReadStream(body, bufferThreshold, _getTempDirectory);
73+
var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
7374
section.Body = fileStream;
7475
registerForDispose(fileStream);
7576
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http.Features;
8+
9+
namespace Microsoft.AspNetCore.Http
10+
{
11+
public static class RequestFormReaderExtensions
12+
{
13+
/// <summary>
14+
/// Read the request body as a form with the given options. These options will only be used
15+
/// if the form has not already been read.
16+
/// </summary>
17+
/// <param name="request">The request.</param>
18+
/// <param name="options">Options for reading the form.</param>
19+
/// <param name="cancellationToken"></param>
20+
/// <returns>The parsed form.</returns>
21+
public static Task<IFormCollection> ReadFormAsync(this HttpRequest request, FormOptions options,
22+
CancellationToken cancellationToken = new CancellationToken())
23+
{
24+
if (request == null)
25+
{
26+
throw new ArgumentNullException(nameof(request));
27+
}
28+
if (options == null)
29+
{
30+
throw new ArgumentNullException(nameof(options));
31+
}
32+
33+
if (!request.HasFormContentType)
34+
{
35+
throw new InvalidOperationException("Incorrect Content-Type: " + request.ContentType);
36+
}
37+
38+
var features = request.HttpContext.Features;
39+
var formFeature = features.Get<IFormFeature>();
40+
if (formFeature == null || formFeature.Form == null)
41+
{
42+
// We haven't read the form yet, replace the reader with one using our own options.
43+
features.Set<IFormFeature>(new FormFeature(request, options));
44+
}
45+
return request.ReadFormAsync(cancellationToken);
46+
}
47+
}
48+
}

src/Microsoft.AspNetCore.Http/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
2222
"Microsoft.AspNetCore.WebUtilities": "1.0.0-*",
2323
"Microsoft.Extensions.ObjectPool": "1.0.0-*",
24+
"Microsoft.Extensions.Options": "1.0.0-*",
2425
"Microsoft.Net.Http.Headers": "1.0.0-*",
2526
"System.Buffers": "4.0.0-*"
2627
},

src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ public string ReadLine(int lengthLimit)
352352
{
353353
if (builder.Length > lengthLimit)
354354
{
355-
throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit.ToString());
355+
throw new InvalidDataException($"Line length limit {lengthLimit} exceeded.");
356356
}
357357
ProcessLineChar(builder, ref foundCR, ref foundCRLF);
358358
}
@@ -372,7 +372,7 @@ public async Task<string> ReadLineAsync(int lengthLimit, CancellationToken cance
372372
{
373373
if (builder.Length > lengthLimit)
374374
{
375-
throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit.ToString());
375+
throw new InvalidDataException($"Line length limit {lengthLimit} exceeded.");
376376
}
377377

378378
ProcessLineChar(builder, ref foundCR, ref foundCRLF);

0 commit comments

Comments
 (0)