// 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.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Net.Http.Formatting.Parsers; using System.Threading; using System.Threading.Tasks; using System.Web.Http; namespace System.Net.Http { /// /// Extension methods to read MIME multipart entities from instances. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class HttpContentMultipartExtensions { private const int MinBufferSize = 256; private const int DefaultBufferSize = 32 * 1024; /// /// Determines whether the specified content is MIME multipart content. /// /// The content. /// /// true if the specified content is MIME multipart content; otherwise, false. /// public static bool IsMimeMultipartContent(this HttpContent content) { if (content == null) { throw Error.ArgumentNull("content"); } return MimeMultipartBodyPartParser.IsMimeMultipartContent(content); } /// /// Determines whether the specified content is MIME multipart content with the /// specified subtype. For example, the subtype mixed would match content /// with a content type of multipart/mixed. /// /// The content. /// The MIME multipart subtype to match. /// /// true if the specified content is MIME multipart content with the specified subtype; otherwise, false. /// public static bool IsMimeMultipartContent(this HttpContent content, string subtype) { if (String.IsNullOrWhiteSpace(subtype)) { throw Error.ArgumentNull("subtype"); } if (IsMimeMultipartContent(content)) { if (content.Headers.ContentType.MediaType.Equals("multipart/" + subtype, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Reads all body parts within a MIME multipart message into memory using a . /// /// An existing instance to use for the object's content. /// A representing the tasks of getting the result of reading the MIME content. public static Task ReadAsMultipartAsync(this HttpContent content) { return ReadAsMultipartAsync(content, new MultipartMemoryStreamProvider(), DefaultBufferSize); } /// /// Reads all body parts within a MIME multipart message into memory using a . /// /// An existing instance to use for the object's content. /// The token to monitor for cancellation requests. /// A representing the tasks of getting the result of reading the MIME content. public static Task ReadAsMultipartAsync(this HttpContent content, CancellationToken cancellationToken) { return ReadAsMultipartAsync(content, new MultipartMemoryStreamProvider(), DefaultBufferSize, cancellationToken); } /// /// Reads all body parts within a MIME multipart message using the provided instance /// to determine where the contents of each body part is written. /// /// The with which to process the data. /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// A representing the tasks of getting the result of reading the MIME content. public static Task ReadAsMultipartAsync(this HttpContent content, T streamProvider) where T : MultipartStreamProvider { return ReadAsMultipartAsync(content, streamProvider, DefaultBufferSize); } /// /// Reads all body parts within a MIME multipart message using the provided instance /// to determine where the contents of each body part is written. /// /// The with which to process the data. /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// The token to monitor for cancellation requests. /// A representing the tasks of getting the result of reading the MIME content. public static Task ReadAsMultipartAsync(this HttpContent content, T streamProvider, CancellationToken cancellationToken) where T : MultipartStreamProvider { return ReadAsMultipartAsync(content, streamProvider, DefaultBufferSize, cancellationToken); } /// /// Reads all body parts within a MIME multipart message using the provided instance /// to determine where the contents of each body part is written and as read buffer size. /// /// The with which to process the data. /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// Size of the buffer used to read the contents. /// A representing the tasks of getting the result of reading the MIME content. public static Task ReadAsMultipartAsync(this HttpContent content, T streamProvider, int bufferSize) where T : MultipartStreamProvider { return ReadAsMultipartAsync(content, streamProvider, bufferSize, CancellationToken.None); } /// /// Reads all body parts within a MIME multipart message using the provided instance /// to determine where the contents of each body part is written and as read buffer size. /// /// The with which to process the data. /// An existing instance to use for the object's content. /// A stream provider providing output streams for where to write body parts as they are parsed. /// Size of the buffer used to read the contents. /// The token to monitor for cancellation requests. /// A representing the tasks of getting the result of reading the MIME content. public static async Task ReadAsMultipartAsync(this HttpContent content, T streamProvider, int bufferSize, CancellationToken cancellationToken) where T : MultipartStreamProvider { if (content == null) { throw Error.ArgumentNull("content"); } if (streamProvider == null) { throw Error.ArgumentNull("streamProvider"); } if (bufferSize < MinBufferSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize); } Stream stream; try { stream = await content.ReadAsStreamAsync(); } catch (Exception e) { throw new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e); } using (var parser = new MimeMultipartBodyPartParser(content, streamProvider)) { byte[] data = new byte[bufferSize]; MultipartAsyncContext context = new MultipartAsyncContext(stream, parser, data, streamProvider.Contents); // Start async read/write loop await MultipartReadAsync(context, cancellationToken); // Let the stream provider post-process when everything is complete await streamProvider.ExecutePostProcessingAsync(cancellationToken); return streamProvider; } } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")] private static async Task MultipartReadAsync(MultipartAsyncContext context, CancellationToken cancellationToken) { Contract.Assert(context != null, "context cannot be null"); while (true) { int bytesRead; try { bytesRead = await context.ContentStream.ReadAsync(context.Data, 0, context.Data.Length, cancellationToken); } catch (Exception e) { throw new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e); } IEnumerable parts = context.MimeParser.ParseBuffer(context.Data, bytesRead); foreach (MimeBodyPart part in parts) { foreach (ArraySegment segment in part.Segments) { try { await part.WriteSegment(segment, cancellationToken); } catch (Exception e) { part.Dispose(); throw new IOException(Properties.Resources.ReadAsMimeMultipartErrorWriting, e); } } if (CheckIsFinalPart(part, context.Result)) { return; } } } } private static bool CheckIsFinalPart(MimeBodyPart part, ICollection result) { Contract.Assert(part != null, "part cannot be null."); Contract.Assert(result != null, "result cannot be null."); if (part.IsComplete) { HttpContent partContent = part.GetCompletedHttpContent(); if (partContent != null) { result.Add(partContent); } bool isFinal = part.IsFinal; part.Dispose(); return isFinal; } return false; } /// /// Managing state for asynchronous read and write operations /// private class MultipartAsyncContext { public MultipartAsyncContext(Stream contentStream, MimeMultipartBodyPartParser mimeParser, byte[] data, ICollection result) { Contract.Assert(contentStream != null); Contract.Assert(mimeParser != null); Contract.Assert(data != null); ContentStream = contentStream; Result = result; MimeParser = mimeParser; Data = data; } /// /// Gets the that we read from. /// public Stream ContentStream { get; private set; } /// /// Gets the collection of parsed instances. /// public ICollection Result { get; private set; } /// /// The data buffer that we use for reading data from the input stream into before processing. /// public byte[] Data { get; private set; } /// /// Gets the MIME parser instance used to parse the data /// public MimeMultipartBodyPartParser MimeParser { get; private set; } } } }