// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http.Formatting.Parsers;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace System.Net.Http
{
///
/// Extension methods to read and entities from instances.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public static class HttpContentMessageExtensions
{
private const int MinBufferSize = 256;
private const int DefaultBufferSize = 32 * 1024;
///
/// Determines whether the specified content is HTTP request message content.
///
/// The content.
///
/// true if the specified content is HTTP message content; otherwise, false.
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
public static bool IsHttpRequestMessageContent(this HttpContent content)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
try
{
return HttpMessageContent.ValidateHttpMessageContent(content, true, false);
}
catch (Exception)
{
return false;
}
}
///
/// Determines whether the specified content is HTTP response message content.
///
/// The content.
///
/// true if the specified content is HTTP message content; otherwise, false.
///
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
public static bool IsHttpResponseMessageContent(this HttpContent content)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
try
{
return HttpMessageContent.ValidateHttpMessageContent(content, false, false);
}
catch (Exception)
{
return false;
}
}
///
/// Read the as an .
///
/// The content to read.
/// The parsed instance.
public static Task ReadAsHttpRequestMessageAsync(this HttpContent content)
{
return ReadAsHttpRequestMessageAsync(content, Uri.UriSchemeHttp, DefaultBufferSize);
}
///
/// Read the as an .
///
/// The content to read.
/// The URI scheme to use for the request URI.
/// The parsed instance.
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
public static Task ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme)
{
return ReadAsHttpRequestMessageAsync(content, uriScheme, DefaultBufferSize);
}
///
/// Read the as an .
///
/// The content to read.
/// The URI scheme to use for the request URI (the
/// URI scheme is not actually part of the HTTP Request URI and so must be provided externally).
/// Size of the buffer.
/// The parsed instance.
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
public static Task ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme, int bufferSize)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
if (uriScheme == null)
{
throw new ArgumentNullException("uriScheme");
}
if (!Uri.CheckSchemeName(uriScheme))
{
throw new ArgumentException(RS.Format(Properties.Resources.HttpMessageParserInvalidUriScheme, uriScheme, typeof(Uri).Name), "uriScheme");
}
if (bufferSize < MinBufferSize)
{
throw new ArgumentOutOfRangeException("bufferSize", bufferSize, RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, MinBufferSize));
}
HttpMessageContent.ValidateHttpMessageContent(content, true, true);
return content.ReadAsStreamAsync().Then(stream =>
{
HttpUnsortedRequest httpRequest = new HttpUnsortedRequest();
HttpRequestHeaderParser parser = new HttpRequestHeaderParser(httpRequest);
ParserState parseStatus;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
int headerConsumed = 0;
while (true)
{
try
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
}
catch (Exception e)
{
throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
}
try
{
parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
}
catch (Exception)
{
parseStatus = ParserState.Invalid;
}
if (parseStatus == ParserState.Done)
{
return CreateHttpRequestMessage(uriScheme, httpRequest, stream, bytesRead - headerConsumed);
}
else if (parseStatus != ParserState.NeedMoreData)
{
throw new IOException(RS.Format(Properties.Resources.HttpMessageParserError, headerConsumed, buffer));
}
}
});
}
///
/// Read the as an .
///
/// The content to read.
/// The parsed instance.
public static Task ReadAsHttpResponseMessageAsync(this HttpContent content)
{
return ReadAsHttpResponseMessageAsync(content, DefaultBufferSize);
}
///
/// Read the as an .
///
/// The content to read.
/// Size of the buffer.
/// The parsed instance.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize)
{
if (content == null)
{
throw new ArgumentNullException("content");
}
if (bufferSize < MinBufferSize)
{
throw new ArgumentOutOfRangeException("bufferSize", bufferSize, RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, MinBufferSize));
}
HttpMessageContent.ValidateHttpMessageContent(content, false, true);
return content.ReadAsStreamAsync().Then(stream =>
{
HttpUnsortedResponse httpResponse = new HttpUnsortedResponse();
HttpResponseHeaderParser parser = new HttpResponseHeaderParser(httpResponse);
ParserState parseStatus;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
int headerConsumed = 0;
while (true)
{
try
{
bytesRead = stream.Read(buffer, 0, buffer.Length);
}
catch (Exception e)
{
throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
}
try
{
parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
}
catch (Exception)
{
parseStatus = ParserState.Invalid;
}
if (parseStatus == ParserState.Done)
{
// Create and return parsed HttpResponseMessage
return CreateHttpResponseMessage(httpResponse, stream, bytesRead - headerConsumed);
}
else if (parseStatus != ParserState.NeedMoreData)
{
throw new IOException(RS.Format(Properties.Resources.HttpMessageParserError, headerConsumed, buffer));
}
}
});
}
///
/// Creates the request URI by combining scheme (provided) with parsed values of
/// host and path.
///
/// The URI scheme to use for the request URI.
/// The unsorted HTTP request.
/// A fully qualified request URI.
private static Uri CreateRequestUri(string uriScheme, HttpUnsortedRequest httpRequest)
{
Contract.Assert(httpRequest != null, "httpRequest cannot be null.");
Contract.Assert(uriScheme != null, "uriScheme cannot be null");
IEnumerable hostValues;
if (httpRequest.HttpHeaders.TryGetValues(FormattingUtilities.HttpHostHeader, out hostValues))
{
int hostCount = hostValues.Count();
if (hostCount != 1)
{
throw new IOException(RS.Format(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, hostCount));
}
}
else
{
throw new IOException(RS.Format(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, 0));
}
// We don't use UriBuilder as hostValues.ElementAt(0) contains 'host:port' and UriBuilder needs these split out into separate host and port.
string requestUri = String.Format(CultureInfo.InvariantCulture, "{0}://{1}{2}", uriScheme, hostValues.ElementAt(0), httpRequest.RequestUri);
return new Uri(requestUri);
}
///
/// Copies the unsorted header fields to a sorted collection.
///
/// The unsorted source headers
/// The destination or .
/// The input used to form any being part of this HTTP request.
/// Start location of any request entity within the .
/// An instance if header fields contained and .
private static HttpContent CreateHeaderFields(HttpHeaders source, HttpHeaders destination, Stream contentStream, int rewind)
{
Contract.Assert(source != null, "source headers cannot be null");
Contract.Assert(destination != null, "destination headers cannot be null");
Contract.Assert(contentStream != null, "contentStream must be non null");
HttpContentHeaders contentHeaders = null;
HttpContent content = null;
// Set the header fields
foreach (KeyValuePair> header in source)
{
if (!destination.TryAddWithoutValidation(header.Key, header.Value))
{
if (contentHeaders == null)
{
contentHeaders = FormattingUtilities.CreateEmptyContentHeaders();
}
contentHeaders.TryAddWithoutValidation(header.Key, header.Value);
}
}
// If we have content headers then create an HttpContent for this Response
if (contentHeaders != null)
{
// Need to rewind the input stream to be at the position right after the HTTP header
// which we may already have parsed as we read the content stream.
if (!contentStream.CanSeek)
{
throw new IOException(RS.Format(Properties.Resources.HttpMessageContentStreamMustBeSeekable, "ContentReadStream", FormattingUtilities.HttpResponseMessageType.Name));
}
contentStream.Seek(0 - rewind, SeekOrigin.Current);
content = new StreamContent(contentStream);
contentHeaders.CopyTo(content.Headers);
}
return content;
}
///
/// Creates an based on information provided in .
///
/// The URI scheme to use for the request URI.
/// The unsorted HTTP request.
/// The input used to form any being part of this HTTP request.
/// Start location of any request entity within the .
/// A newly created instance.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
private static HttpRequestMessage CreateHttpRequestMessage(string uriScheme, HttpUnsortedRequest httpRequest, Stream contentStream, int rewind)
{
Contract.Assert(uriScheme != null, "URI scheme must be non null");
Contract.Assert(httpRequest != null, "httpRequest must be non null");
Contract.Assert(contentStream != null, "contentStream must be non null");
HttpRequestMessage httpRequestMessage = new HttpRequestMessage();
// Set method, requestURI, and version
httpRequestMessage.Method = httpRequest.Method;
httpRequestMessage.RequestUri = CreateRequestUri(uriScheme, httpRequest);
httpRequestMessage.Version = httpRequest.Version;
// Set the header fields and content if any
httpRequestMessage.Content = CreateHeaderFields(httpRequest.HttpHeaders, httpRequestMessage.Headers, contentStream, rewind);
return httpRequestMessage;
}
///
/// Creates an based on information provided in .
///
/// The unsorted HTTP Response.
/// The input used to form any being part of this HTTP Response.
/// Start location of any Response entity within the .
/// A newly created instance.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
private static HttpResponseMessage CreateHttpResponseMessage(HttpUnsortedResponse httpResponse, Stream contentStream, int rewind)
{
Contract.Assert(httpResponse != null, "httpResponse must be non null");
Contract.Assert(contentStream != null, "contentStream must be non null");
HttpResponseMessage httpResponseMessage = new HttpResponseMessage();
// Set version, status code and reason phrase
httpResponseMessage.Version = httpResponse.Version;
httpResponseMessage.StatusCode = httpResponse.StatusCode;
httpResponseMessage.ReasonPhrase = httpResponse.ReasonPhrase;
// Set the header fields and content if any
httpResponseMessage.Content = CreateHeaderFields(httpResponse.HttpHeaders, httpResponseMessage.Headers, contentStream, rewind);
return httpResponseMessage;
}
}
}