// 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; } } }