From e64b8e55e57e18fd6295652f8c845b5143fd69da Mon Sep 17 00:00:00 2001 From: Victor Hurdugaci Date: Wed, 6 Jul 2016 14:40:48 -0700 Subject: [PATCH] Add some multipart reader utilities to make reading streams easier --- .../HttpRequestMultipartExtensions.cs | 26 +++++++ .../Features/FormFeature.cs | 38 +++++----- .../FileMultipartSection.cs | 70 ++++++++++++++++++ .../FormMultipartSection.cs | 63 ++++++++++++++++ .../MultipartSectionConverterExtensions.cs | 74 +++++++++++++++++++ .../MultipartSectionStreamExtensions.cs | 49 ++++++++++++ .../project.json | 1 + ...ispositionHeaderValueIdentityExtensions.cs | 45 +++++++++++ 8 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs create mode 100644 src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs create mode 100644 src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs create mode 100644 src/Microsoft.AspNetCore.WebUtilities/MultipartSectionConverterExtensions.cs create mode 100644 src/Microsoft.AspNetCore.WebUtilities/MultipartSectionStreamExtensions.cs create mode 100644 src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs 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); + } + } +}