diff --git a/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs b/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs
new file mode 100644
index 0000000000..76770428a0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Extensions
+{
+ public static class HttpRequestMultipartExtensions
+ {
+ public static string GetMultipartBoundary(this HttpRequest request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ MediaTypeHeaderValue mediaType;
+ if (!MediaTypeHeaderValue.TryParse(request.ContentType, out mediaType))
+ {
+ return string.Empty;
+ }
+ return HeaderUtilities.RemoveQuotes(mediaType.Boundary);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs
index ff1fd9ccf3..cd8b491ffd 100644
--- a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs
+++ b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs
@@ -170,20 +170,24 @@ namespace Microsoft.AspNetCore.Http.Features
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
while (section != null)
{
+ // Parse the content disposition here and pass it further to avoid reparsings
ContentDispositionHeaderValue contentDisposition;
ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
- if (HasFileContentDisposition(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,
+ section.EnableRewind(
+ _request.HttpContext.Response.RegisterForDispose,
_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);
+
// Find the end
await section.Body.DrainAsync(cancellationToken);
- var name = HeaderUtilities.RemoveQuotes(contentDisposition.Name) ?? string.Empty;
- var fileName = HeaderUtilities.RemoveQuotes(contentDisposition.FileNameStar) ??
- HeaderUtilities.RemoveQuotes(contentDisposition.FileName) ??
- string.Empty;
+ var name = fileSection.Name;
+ var fileName = fileSection.FileName;
FormFile file;
if (section.BaseStreamOffset.HasValue)
@@ -208,26 +212,22 @@ namespace Microsoft.AspNetCore.Http.Features
}
files.Add(file);
}
- else if (HasFormDataContentDisposition(contentDisposition))
+ 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 mulipart headers length limit is already in effect.
- var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
- MediaTypeHeaderValue mediaType;
- MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
- var encoding = FilterEncoding(mediaType?.Encoding);
- using (var reader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
+ var key = formDataSection.Name;
+ var value = await formDataSection.GetValueAsync();
+
+ formAccumulator.Append(key, value);
+ if (formAccumulator.ValueCount > _options.ValueCountLimit)
{
- // The value length limit is enforced by MultipartBodyLengthLimit
- var value = await reader.ReadToEndAsync();
- formAccumulator.Append(key, value);
- if (formAccumulator.ValueCount > _options.ValueCountLimit)
- {
- throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded.");
- }
+ throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded.");
}
}
else
diff --git a/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs b/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs
new file mode 100644
index 0000000000..b1ba2ff47e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs
@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.WebUtilities
+{
+ ///
+ /// Represents a file multipart section
+ ///
+ public class FileMultipartSection
+ {
+ private ContentDispositionHeaderValue _contentDispositionHeader;
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The section from which to create the
+ /// Reparses the content disposition header
+ public FileMultipartSection(MultipartSection section)
+ :this(section, section.GetContentDispositionHeader())
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The section from which to create the
+ /// An already parsed content disposition header
+ public FileMultipartSection(MultipartSection section, ContentDispositionHeaderValue header)
+ {
+ if (!header.IsFileDisposition())
+ {
+ throw new ArgumentException($"Argument must be a file section", nameof(section));
+ }
+
+ Section = section;
+ _contentDispositionHeader = header;
+
+ Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name) ?? string.Empty;
+ FileName = HeaderUtilities.RemoveQuotes(
+ _contentDispositionHeader.FileNameStar ??
+ _contentDispositionHeader.FileName ??
+ string.Empty);
+ }
+
+ ///
+ /// Gets the original section from which this object was created
+ ///
+ public MultipartSection Section { get; }
+
+ ///
+ /// Gets the file stream from the section body
+ ///
+ public Stream FileStream => Section.Body;
+
+ ///
+ /// Gets the name of the section
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the name of the file from the section
+ ///
+ public string FileName { get; }
+
+ }
+}
diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs b/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs
new file mode 100644
index 0000000000..652cbdeb12
--- /dev/null
+++ b/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs
@@ -0,0 +1,63 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.WebUtilities
+{
+ ///
+ /// Represents a form multipart section
+ ///
+ public class FormMultipartSection
+ {
+ private ContentDispositionHeaderValue _contentDispositionHeader;
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The section from which to create the
+ /// Reparses the content disposition header
+ public FormMultipartSection(MultipartSection section)
+ : this(section, section.GetContentDispositionHeader())
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The section from which to create the
+ /// An already parsed content disposition header
+ public FormMultipartSection(MultipartSection section, ContentDispositionHeaderValue header)
+ {
+ if (header == null || !header.IsFormDisposition())
+ {
+ throw new ArgumentException($"Argument must be a form section", nameof(section));
+ }
+
+ Section = section;
+ _contentDispositionHeader = header;
+ Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name);
+ }
+
+ ///
+ /// Gets the original section from which this object was created
+ ///
+ public MultipartSection Section { get; }
+
+ ///
+ /// The form name
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the form value
+ ///
+ /// The form value
+ public Task GetValueAsync()
+ {
+ return Section.ReadAsStringAsync();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionConverterExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionConverterExtensions.cs
new file mode 100644
index 0000000000..826ced168e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionConverterExtensions.cs
@@ -0,0 +1,74 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.WebUtilities
+{
+ ///
+ /// Various extensions for converting multipart sections
+ ///
+ public static class MultipartSectionConverterExtensions
+ {
+ ///
+ /// Converts the section to a file section
+ ///
+ /// The section to convert
+ /// A file section
+ public static FileMultipartSection AsFileSection(this MultipartSection section)
+ {
+ if (section == null)
+ {
+ throw new ArgumentNullException(nameof(section));
+ }
+
+ try
+ {
+ return new FileMultipartSection(section);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts the section to a form section
+ ///
+ /// The section to convert
+ /// A form section
+ public static FormMultipartSection AsFormDataSection(this MultipartSection section)
+ {
+ if (section == null)
+ {
+ throw new ArgumentNullException(nameof(section));
+ }
+
+ try
+ {
+ return new FormMultipartSection(section);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Retrieves and parses the content disposition header from a section
+ ///
+ /// The section from which to retrieve
+ /// A if the header was found, null otherwise
+ public static ContentDispositionHeaderValue GetContentDispositionHeader(this MultipartSection section)
+ {
+ ContentDispositionHeaderValue header;
+ if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out header))
+ {
+ return null;
+ }
+
+ return header;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionStreamExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionStreamExtensions.cs
new file mode 100644
index 0000000000..463a8d88d6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartSectionStreamExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.WebUtilities
+{
+ ///
+ /// Various extension methods for dealing with the section body stream
+ ///
+ public static class MultipartSectionStreamExtensions
+ {
+ ///
+ /// Reads the body of the section as a string
+ ///
+ /// The section to read from
+ /// The body steam as string
+ public static async Task ReadAsStringAsync(this MultipartSection section)
+ {
+ if (section == null)
+ {
+ throw new ArgumentNullException(nameof(section));
+ }
+
+ MediaTypeHeaderValue sectionMediaType;
+ MediaTypeHeaderValue.TryParse(section.ContentType, out sectionMediaType);
+
+ Encoding streamEncoding = sectionMediaType?.Encoding;
+ if (streamEncoding == null || streamEncoding == Encoding.UTF7)
+ {
+ streamEncoding = Encoding.UTF8;
+ }
+
+ using (var reader = new StreamReader(
+ section.Body,
+ streamEncoding,
+ detectEncodingFromByteOrderMarks: true,
+ bufferSize: 1024,
+ leaveOpen: true))
+ {
+ return await reader.ReadToEndAsync();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.WebUtilities/project.json b/src/Microsoft.AspNetCore.WebUtilities/project.json
index 043063c90a..49d03defca 100644
--- a/src/Microsoft.AspNetCore.WebUtilities/project.json
+++ b/src/Microsoft.AspNetCore.WebUtilities/project.json
@@ -25,6 +25,7 @@
"type": "build",
"version": "1.1.0-*"
},
+ "Microsoft.Net.Http.Headers": "1.1.0-*",
"System.Buffers": "4.0.0-*",
"System.Text.Encodings.Web": "4.0.0-*"
},
diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs
new file mode 100644
index 0000000000..0a275e55b8
--- /dev/null
+++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.Net.Http.Headers
+{
+ ///
+ /// Various extension methods for for identifying the type of the disposition header
+ ///
+ public static class ContentDispositionHeaderValueIdentityExtensions
+ {
+ ///
+ /// Checks if the content disposition header is a file disposition
+ ///
+ /// The header to check
+ /// True if the header is file disposition, false otherwise
+ public static bool IsFileDisposition(this ContentDispositionHeaderValue header)
+ {
+ if (header == null)
+ {
+ throw new ArgumentNullException(nameof(header));
+ }
+
+ return header.DispositionType.Equals("form-data")
+ && (!string.IsNullOrEmpty(header.FileName) || !string.IsNullOrEmpty(header.FileNameStar));
+ }
+
+ ///
+ /// Checks if the content disposition header is a form disposition
+ ///
+ /// The header to check
+ /// True if the header is form disposition, false otherwise
+ public static bool IsFormDisposition(this ContentDispositionHeaderValue header)
+ {
+ if (header == null)
+ {
+ throw new ArgumentNullException(nameof(header));
+ }
+
+ return header.DispositionType.Equals("form-data")
+ && string.IsNullOrEmpty(header.FileName) && string.IsNullOrEmpty(header.FileNameStar);
+ }
+ }
+}