Add some multipart reader utilities to make reading streams easier

This commit is contained in:
Victor Hurdugaci 2016-07-06 14:40:48 -07:00
parent 9a28932b7a
commit e64b8e55e5
8 changed files with 347 additions and 19 deletions

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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
{
/// <summary>
/// Represents a file multipart section
/// </summary>
public class FileMultipartSection
{
private ContentDispositionHeaderValue _contentDispositionHeader;
/// <summary>
/// Creates a new instance of the <see cref="FileMultipartSection"/> class
/// </summary>
/// <param name="section">The section from which to create the <see cref="FileMultipartSection"/></param>
/// <remarks>Reparses the content disposition header</remarks>
public FileMultipartSection(MultipartSection section)
:this(section, section.GetContentDispositionHeader())
{
}
/// <summary>
/// Creates a new instance of the <see cref="FileMultipartSection"/> class
/// </summary>
/// <param name="section">The section from which to create the <see cref="FileMultipartSection"/></param>
/// <param name="header">An already parsed content disposition header</param>
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);
}
/// <summary>
/// Gets the original section from which this object was created
/// </summary>
public MultipartSection Section { get; }
/// <summary>
/// Gets the file stream from the section body
/// </summary>
public Stream FileStream => Section.Body;
/// <summary>
/// Gets the name of the section
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the name of the file from the section
/// </summary>
public string FileName { get; }
}
}

View File

@ -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
{
/// <summary>
/// Represents a form multipart section
/// </summary>
public class FormMultipartSection
{
private ContentDispositionHeaderValue _contentDispositionHeader;
/// <summary>
/// Creates a new instance of the <see cref="FormMultipartSection"/> class
/// </summary>
/// <param name="section">The section from which to create the <see cref="FormMultipartSection"/></param>
/// <remarks>Reparses the content disposition header</remarks>
public FormMultipartSection(MultipartSection section)
: this(section, section.GetContentDispositionHeader())
{
}
/// <summary>
/// Creates a new instance of the <see cref="FormMultipartSection"/> class
/// </summary>
/// <param name="section">The section from which to create the <see cref="FormMultipartSection"/></param>
/// <param name="header">An already parsed content disposition header</param>
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);
}
/// <summary>
/// Gets the original section from which this object was created
/// </summary>
public MultipartSection Section { get; }
/// <summary>
/// The form name
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the form value
/// </summary>
/// <returns>The form value</returns>
public Task<string> GetValueAsync()
{
return Section.ReadAsStringAsync();
}
}
}

View File

@ -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
{
/// <summary>
/// Various extensions for converting multipart sections
/// </summary>
public static class MultipartSectionConverterExtensions
{
/// <summary>
/// Converts the section to a file section
/// </summary>
/// <param name="section">The section to convert</param>
/// <returns>A file section</returns>
public static FileMultipartSection AsFileSection(this MultipartSection section)
{
if (section == null)
{
throw new ArgumentNullException(nameof(section));
}
try
{
return new FileMultipartSection(section);
}
catch
{
return null;
}
}
/// <summary>
/// Converts the section to a form section
/// </summary>
/// <param name="section">The section to convert</param>
/// <returns>A form section</returns>
public static FormMultipartSection AsFormDataSection(this MultipartSection section)
{
if (section == null)
{
throw new ArgumentNullException(nameof(section));
}
try
{
return new FormMultipartSection(section);
}
catch
{
return null;
}
}
/// <summary>
/// Retrieves and parses the content disposition header from a section
/// </summary>
/// <param name="section">The section from which to retrieve</param>
/// <returns>A <see cref="ContentDispositionHeaderValue"/> if the header was found, null otherwise</returns>
public static ContentDispositionHeaderValue GetContentDispositionHeader(this MultipartSection section)
{
ContentDispositionHeaderValue header;
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out header))
{
return null;
}
return header;
}
}
}

View File

@ -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
{
/// <summary>
/// Various extension methods for dealing with the section body stream
/// </summary>
public static class MultipartSectionStreamExtensions
{
/// <summary>
/// Reads the body of the section as a string
/// </summary>
/// <param name="section">The section to read from</param>
/// <returns>The body steam as string</returns>
public static async Task<string> 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();
}
}
}
}

View File

@ -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-*"
},

View File

@ -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
{
/// <summary>
/// Various extension methods for <see cref="ContentDispositionHeaderValue"/> for identifying the type of the disposition header
/// </summary>
public static class ContentDispositionHeaderValueIdentityExtensions
{
/// <summary>
/// Checks if the content disposition header is a file disposition
/// </summary>
/// <param name="header">The header to check</param>
/// <returns>True if the header is file disposition, false otherwise</returns>
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));
}
/// <summary>
/// Checks if the content disposition header is a form disposition
/// </summary>
/// <param name="header">The header to check</param>
/// <returns>True if the header is form disposition, false otherwise</returns>
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);
}
}
}