Add some multipart reader utilities to make reading streams easier
This commit is contained in:
parent
9a28932b7a
commit
e64b8e55e5
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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-*"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue