// 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.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; using System.Threading.Tasks; using System.Web.Http; 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 Error.ArgumentNull("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 Error.ArgumentNull("content"); } try { return HttpMessageContent.ValidateHttpMessageContent(content, false, false); } catch (Exception) { return false; } } /// /// Read the as an . /// /// The content to read. /// A task object representing reading the content as an . public static Task ReadAsHttpRequestMessageAsync(this HttpContent content) { return ReadAsHttpRequestMessageAsync(content, "http", DefaultBufferSize); } /// /// Read the as an . /// /// The content to read. /// The token to monitor for cancellation requests. /// A task object representing reading the content as an . public static Task ReadAsHttpRequestMessageAsync(this HttpContent content, CancellationToken cancellationToken) { return ReadAsHttpRequestMessageAsync(content, "http", DefaultBufferSize, cancellationToken); } /// /// Read the as an . /// /// The content to read. /// The URI scheme to use for the request URI. /// A task object representing reading the content as an . [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 token to monitor for cancellation requests. /// A task object representing reading the content as an . [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, CancellationToken cancellationToken) { return ReadAsHttpRequestMessageAsync(content, uriScheme, DefaultBufferSize, cancellationToken); } /// /// 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. /// A task object representing reading the content as an . [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, int bufferSize) { return ReadAsHttpRequestMessageAsync(content, uriScheme, bufferSize, HttpRequestHeaderParser.DefaultMaxHeaderSize); } /// /// 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 token to monitor for cancellation requests. /// A task object representing reading the content as an . [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, int bufferSize, CancellationToken cancellationToken) { return ReadAsHttpRequestMessageAsync(content, uriScheme, bufferSize, HttpRequestHeaderParser.DefaultMaxHeaderSize, cancellationToken); } /// /// 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 max length of the HTTP header. /// A task object representing reading the content as an . [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, int maxHeaderSize) { return ReadAsHttpRequestMessageAsync(content, uriScheme, bufferSize, maxHeaderSize, CancellationToken.None); } /// /// 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 max length of the HTTP header. /// The token to monitor for cancellation requests. /// A task object representing reading the content as an . [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, int maxHeaderSize, CancellationToken cancellationToken) { if (content == null) { throw Error.ArgumentNull("content"); } if (uriScheme == null) { throw Error.ArgumentNull("uriScheme"); } if (!Uri.CheckSchemeName(uriScheme)) { throw Error.Argument("uriScheme", Properties.Resources.HttpMessageParserInvalidUriScheme, uriScheme, typeof(Uri).Name); } if (bufferSize < MinBufferSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize); } if (maxHeaderSize < InternetMessageFormatHeaderParser.MinHeaderSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("maxHeaderSize", maxHeaderSize, InternetMessageFormatHeaderParser.MinHeaderSize); } HttpMessageContent.ValidateHttpMessageContent(content, true, true); return content.ReadAsHttpRequestMessageAsyncCore(uriScheme, bufferSize, maxHeaderSize, cancellationToken); } private static async Task ReadAsHttpRequestMessageAsyncCore(this HttpContent content, string uriScheme, int bufferSize, int maxHeaderSize, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Stream stream = await content.ReadAsStreamAsync(); HttpUnsortedRequest httpRequest = new HttpUnsortedRequest(); HttpRequestHeaderParser parser = new HttpRequestHeaderParser(httpRequest, Math.Max(HttpRequestHeaderParser.DefaultMaxRequestLineSize, maxHeaderSize), maxHeaderSize); ParserState parseStatus; byte[] buffer = new byte[bufferSize]; int bytesRead = 0; int headerConsumed = 0; while (true) { try { bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); } 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 Error.InvalidOperation(Properties.Resources.HttpMessageParserError, headerConsumed, buffer); } else if (bytesRead == 0) { throw new IOException(Properties.Resources.ReadAsHttpMessageUnexpectedTermination); } } } /// /// Read the as an . /// /// The content to read. /// A task object representing reading the content as an . public static Task ReadAsHttpResponseMessageAsync(this HttpContent content) { return ReadAsHttpResponseMessageAsync(content, DefaultBufferSize); } /// /// Read the as an . /// /// The content to read. /// The token to monitor for cancellation requests. /// A task object representing reading the content as an . public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, CancellationToken cancellationToken) { return ReadAsHttpResponseMessageAsync(content, DefaultBufferSize, cancellationToken); } /// /// Read the as an . /// /// The content to read. /// Size of the buffer. /// A task object representing reading the content as an . public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize) { return ReadAsHttpResponseMessageAsync(content, bufferSize, HttpResponseHeaderParser.DefaultMaxHeaderSize); } /// /// Read the as an . /// /// The content to read. /// Size of the buffer. /// The token to monitor for cancellation requests. /// A task object representing reading the content as an . public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize, CancellationToken cancellationToken) { return ReadAsHttpResponseMessageAsync(content, bufferSize, HttpResponseHeaderParser.DefaultMaxHeaderSize, cancellationToken); } /// /// Read the as an . /// /// The content to read. /// Size of the buffer. /// The max length of the HTTP header. /// A task object representing reading the content as an . [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")] public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize, int maxHeaderSize) { return ReadAsHttpResponseMessageAsync(content, bufferSize, maxHeaderSize, CancellationToken.None); } /// /// Read the as an . /// /// The content to read. /// Size of the buffer. /// The max length of the HTTP header. /// The token to monitor for cancellation requests. /// The parsed instance. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")] public static Task ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize, int maxHeaderSize, CancellationToken cancellationToken) { if (content == null) { throw Error.ArgumentNull("content"); } if (bufferSize < MinBufferSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize); } if (maxHeaderSize < InternetMessageFormatHeaderParser.MinHeaderSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("maxHeaderSize", maxHeaderSize, InternetMessageFormatHeaderParser.MinHeaderSize); } HttpMessageContent.ValidateHttpMessageContent(content, false, true); return content.ReadAsHttpResponseMessageAsyncCore(bufferSize, maxHeaderSize, cancellationToken); } private static async Task ReadAsHttpResponseMessageAsyncCore(this HttpContent content, int bufferSize, int maxHeaderSize, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Stream stream = await content.ReadAsStreamAsync(); HttpUnsortedResponse httpResponse = new HttpUnsortedResponse(); HttpResponseHeaderParser parser = new HttpResponseHeaderParser(httpResponse, HttpResponseHeaderParser.DefaultMaxStatusLineSize, maxHeaderSize); ParserState parseStatus; byte[] buffer = new byte[bufferSize]; int bytesRead = 0; int headerConsumed = 0; while (true) { try { bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); } 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 Error.InvalidOperation(Properties.Resources.HttpMessageParserError, headerConsumed, buffer); } else if (bytesRead == 0) { throw new IOException(Properties.Resources.ReadAsHttpMessageUnexpectedTermination); } } } /// /// 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 Error.InvalidOperation(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, hostCount); } } else { throw Error.InvalidOperation(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 Error.InvalidOperation(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; } } }