/
FormFeature.cs
408 lines (354 loc) · 14.5 KB
/
FormFeature.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Http.Features;
/// <summary>
/// Default implementation for <see cref="IFormFeature"/>.
/// </summary>
public class FormFeature : IFormFeature
{
private readonly HttpRequest? _request;
private readonly Endpoint? _endpoint;
private FormOptions _options;
private Task<IFormCollection>? _parsedFormTask;
private IFormCollection? _form;
private MediaTypeHeaderValue? _formContentType; // null iff _form is null
/// <summary>
/// Initializes a new instance of <see cref="FormFeature"/>.
/// </summary>
/// <param name="form">The <see cref="IFormCollection"/> to use as the backing store.</param>
public FormFeature(IFormCollection form)
{
ArgumentNullException.ThrowIfNull(form);
Form = form;
_formContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
_options = FormOptions.Default;
}
/// <summary>
/// Initializes a new instance of <see cref="FormFeature"/>.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
public FormFeature(HttpRequest request)
: this(request, FormOptions.Default)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FormFeature"/>.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
/// <param name="options">The <see cref="FormOptions"/>.</param>
public FormFeature(HttpRequest request, FormOptions options)
: this(request, options, null)
{
}
internal FormFeature(HttpRequest request, FormOptions options, Endpoint? endpoint)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(options);
_request = request;
_options = options;
_endpoint = endpoint;
}
// Internal for testing.
internal FormOptions FormOptions => _options;
private MediaTypeHeaderValue? ContentType
{
get
{
MediaTypeHeaderValue? mt = null;
if (_request is not null)
{
_ = MediaTypeHeaderValue.TryParse(_request.ContentType, out mt);
}
if (_form is not null && mt is null)
{
mt = _formContentType;
}
return mt;
}
}
/// <inheritdoc />
public bool HasFormContentType
{
get
{
// Set directly
if (Form != null)
{
return true;
}
if (_request is null)
{
return false;
}
var contentType = ContentType;
return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType);
}
}
internal bool HasInvalidAntiforgeryValidationFeature => ResolveHasInvalidAntiforgeryValidationFeature();
/// <inheritdoc />
public IFormCollection? Form
{
get
{
HandleUncheckedAntiforgeryValidationFeature();
return _form;
}
set
{
_parsedFormTask = null;
_form = value;
if (_form is null)
{
_formContentType = null;
}
else
{
_formContentType ??= new MediaTypeHeaderValue("application/x-www-form-urlencoded");
}
}
}
/// <inheritdoc />
public IFormCollection ReadForm()
{
HandleUncheckedAntiforgeryValidationFeature();
if (Form != null)
{
return Form;
}
if (!HasFormContentType)
{
throw new InvalidOperationException("This request does not have a Content-Type header. Forms are available from requests with bodies like POSTs and a form Content-Type of either application/x-www-form-urlencoded or multipart/form-data.");
}
// c.f., https://aka.ms/aspnet/forms-async
return ReadFormAsync().GetAwaiter().GetResult();
}
/// <inheritdoc />
public Task<IFormCollection> ReadFormAsync() => ReadFormAsync(CancellationToken.None);
/// <inheritdoc />
public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
{
HandleUncheckedAntiforgeryValidationFeature();
// Avoid state machine and task allocation for repeated reads
if (_parsedFormTask == null)
{
if (Form != null)
{
_parsedFormTask = Task.FromResult(Form);
}
else
{
_parsedFormTask = InnerReadFormAsync(cancellationToken);
}
}
return _parsedFormTask;
}
private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken)
{
if (_request is null)
{
throw new InvalidOperationException("Cannot read form from this request. Request is 'null'.");
}
HandleUncheckedAntiforgeryValidationFeature();
_options = _endpoint is null ? _options : GetFormOptionsFromMetadata(_options, _endpoint);
if (!HasFormContentType)
{
throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType);
}
cancellationToken.ThrowIfCancellationRequested();
if (_request.ContentLength == 0)
{
return FormCollection.Empty;
}
if (_options.BufferBody)
{
_request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit);
}
FormCollection? formFields = null;
FormFileCollection? files = null;
// Some of these code paths use StreamReader which does not support cancellation tokens.
using (cancellationToken.Register((state) => ((HttpContext)state!).Abort(), _request.HttpContext))
{
var contentType = ContentType;
// Check the content-type
if (HasApplicationFormContentType(contentType))
{
var encoding = FilterEncoding(contentType.Encoding);
var formReader = new FormPipeReader(_request.BodyReader, encoding)
{
ValueCountLimit = _options.ValueCountLimit,
KeyLengthLimit = _options.KeyLengthLimit,
ValueLengthLimit = _options.ValueLengthLimit,
};
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
}
else if (HasMultipartFormContentType(contentType))
{
var formAccumulator = new KeyValueAccumulator();
var sectionCount = 0;
var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit);
var multipartReader = new MultipartReader(boundary, _request.Body)
{
HeadersCountLimit = _options.MultipartHeadersCountLimit,
HeadersLengthLimit = _options.MultipartHeadersLengthLimit,
BodyLengthLimit = _options.MultipartBodyLengthLimit,
};
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
while (section != null)
{
sectionCount++;
if (sectionCount > _options.ValueCountLimit)
{
throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded.");
}
// Parse the content disposition here and pass it further to avoid reparsings
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition))
{
throw new InvalidDataException("Form section has invalid Content-Disposition value: " + section.ContentDisposition);
}
if (contentDisposition.IsFileDisposition())
{
var fileSection = new FileMultipartSection(section, contentDisposition);
// Enable buffering for the file if not already done for the full body
section.EnableRewind(
_request.HttpContext.Response.RegisterForDispose,
_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);
// Find the end
await section.Body.DrainAsync(cancellationToken);
var name = fileSection.Name;
var fileName = fileSection.FileName;
FormFile file;
if (section.BaseStreamOffset.HasValue)
{
// Relative reference to buffered request body
file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName);
}
else
{
// Individually buffered file body
file = new FormFile(section.Body, 0, section.Body.Length, name, fileName);
}
file.Headers = new HeaderDictionary(section.Headers);
if (files == null)
{
files = new FormFileCollection();
}
files.Add(file);
}
else if (contentDisposition.IsFormDisposition())
{
var formDataSection = new FormMultipartSection(section, contentDisposition);
// Content-Disposition: form-data; name="key"
//
// value
// Do not limit the key name length here because the multipart headers length limit is already in effect.
var key = formDataSection.Name;
var value = await formDataSection.GetValueAsync(cancellationToken);
formAccumulator.Append(key, value);
}
else
{
// Ignore form sections with invalid content disposition
}
section = await multipartReader.ReadNextSectionAsync(cancellationToken);
}
if (formAccumulator.HasValues)
{
formFields = new FormCollection(formAccumulator.GetResults(), files);
}
}
}
// Rewind so later readers don't have to.
if (_request.Body.CanSeek)
{
_request.Body.Seek(0, SeekOrigin.Begin);
}
if (formFields != null)
{
Form = formFields;
}
else if (files != null)
{
Form = new FormCollection(null, files);
}
else
{
Form = FormCollection.Empty;
}
return Form;
}
private static Encoding FilterEncoding(Encoding? encoding)
{
// UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases.
// https://learn.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001
if (encoding == null || encoding.CodePage == 65000)
{
return Encoding.UTF8;
}
return encoding;
}
private static bool HasApplicationFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType)
{
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase);
}
private static bool HasMultipartFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType)
{
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase);
}
private bool ResolveHasInvalidAntiforgeryValidationFeature()
{
if (_request is null)
{
return false;
}
var hasInvokedMiddleware = _request.HttpContext.Items.ContainsKey("__AntiforgeryMiddlewareWithEndpointInvoked");
var hasInvalidToken = _request.HttpContext.Features.Get<IAntiforgeryValidationFeature>() is { IsValid: false };
return hasInvokedMiddleware && hasInvalidToken;
}
private void HandleUncheckedAntiforgeryValidationFeature()
{
if (HasInvalidAntiforgeryValidationFeature)
{
throw new InvalidOperationException("This form is being accessed with an invalid anti-forgery token. Validate the `IAntiforgeryValidationFeature` on the request before reading from the form.");
}
}
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
if (StringSegment.IsNullOrEmpty(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary.ToString();
}
private static FormOptions GetFormOptionsFromMetadata(FormOptions baseFormOptions, Endpoint endpoint)
{
var formOptionsMetadatas = endpoint.Metadata
.GetOrderedMetadata<IFormOptionsMetadata>();
var metadataCount = formOptionsMetadatas.Count;
if (metadataCount == 0)
{
return baseFormOptions;
}
var finalFormOptionsMetadata = new MutableFormOptionsMetadata(formOptionsMetadatas[metadataCount - 1]);
for (int i = metadataCount - 2; i >= 0; i--)
{
formOptionsMetadatas[i].MergeWith(ref finalFormOptionsMetadata);
}
return finalFormOptionsMetadata.ResolveFormOptions(baseFormOptions);
}
}