// 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.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IO; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Web.Http; namespace System.Net.Http { /// /// Derived class which can encapsulate an /// or an as an entity with media type "application/http". /// public class HttpMessageContent : HttpContent { private const string SP = " "; private const string ColonSP = ": "; private const string CRLF = "\r\n"; private const string CommaSeparator = ", "; private const int DefaultHeaderAllocation = 2 * 1024; private const string DefaultMediaType = "application/http"; private const string MsgTypeParameter = "msgtype"; private const string DefaultRequestMsgType = "request"; private const string DefaultResponseMsgType = "response"; private const string DefaultRequestMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultRequestMsgType; private const string DefaultResponseMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultResponseMsgType; // Set of header fields that only support single values such as Set-Cookie. private static readonly HashSet _singleValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "Cookie", "Set-Cookie", "X-Powered-By", }; // Set of header fields that should get serialized as space-separated values such as User-Agent. private static readonly HashSet _spaceSeparatedValueHeaderFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "User-Agent", }; private bool _contentConsumed; private Lazy> _streamTask; /// /// Initializes a new instance of the class encapsulating an /// . /// /// The instance to encapsulate. public HttpMessageContent(HttpRequestMessage httpRequest) { if (httpRequest == null) { throw Error.ArgumentNull("httpRequest"); } HttpRequestMessage = httpRequest; Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType)); InitializeStreamTask(); } /// /// Initializes a new instance of the class encapsulating an /// . /// /// The instance to encapsulate. public HttpMessageContent(HttpResponseMessage httpResponse) { if (httpResponse == null) { throw Error.ArgumentNull("httpResponse"); } HttpResponseMessage = httpResponse; Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType); Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType)); InitializeStreamTask(); } private HttpContent Content { get { return HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; } } /// /// Gets the HTTP request message. /// public HttpRequestMessage HttpRequestMessage { get; private set; } /// /// Gets the HTTP response message. /// public HttpResponseMessage HttpResponseMessage { get; private set; } private void InitializeStreamTask() { _streamTask = new Lazy>(() => Content == null ? null : Content.ReadAsStreamAsync()); } /// /// Validates whether the content contains an HTTP Request or an HTTP Response. /// /// The content to validate. /// if set to true if the content is either an HTTP Request or an HTTP Response. /// Indicates whether validation failure should result in an or not. /// true if content is either an HTTP Request or an HTTP Response internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError) { if (content == null) { throw Error.ArgumentNull("content"); } MediaTypeHeaderValue contentType = content.Headers.ContentType; if (contentType != null) { if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase)) { if (throwOnError) { throw Error.Argument("content", Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name, isRequest ? DefaultRequestMediaType : DefaultResponseMediaType); } else { return false; } } foreach (NameValueHeaderValue parameter in contentType.Parameters) { if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase)) { string msgType = FormattingUtilities.UnquoteToken(parameter.Value); if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase)) { if (throwOnError) { throw Error.Argument("content", Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name, isRequest ? DefaultRequestMediaType : DefaultResponseMediaType); } else { return false; } } return true; } } } if (throwOnError) { throw Error.Argument("content", Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name, isRequest ? DefaultRequestMediaType : DefaultResponseMediaType); } else { return false; } } /// /// Asynchronously serializes the object's content to the given . /// /// The to which to write. /// The associated . /// A instance that is asynchronously serializing the object's content. protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { if (stream == null) { throw Error.ArgumentNull("stream"); } byte[] header = SerializeHeader(); await stream.WriteAsync(header, 0, header.Length); if (Content != null) { Stream readStream = await _streamTask.Value; ValidateStreamForReading(readStream); await Content.CopyToAsync(stream); } } /// /// Computes the length of the stream if possible. /// /// The computed length of the stream. /// true if the length has been computed; otherwise false. [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "The code is more readable with such comments")] protected override bool TryComputeLength(out long length) { // We have four states we could be in: // 1. We have content, but the task is still running or finished without success // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length // 4. We don't have content (streamTask.Value == null) // // For #1 and #2, we return false. // For #3, we return true & the size of our headers + the content length // For #4, we return true & the size of our headers bool hasContent = _streamTask.Value != null; length = 0; // Cases #1, #2, #3 if (hasContent) { Stream readStream; if (!_streamTask.Value.TryGetResult(out readStream) // Case #1 || readStream == null || !readStream.CanSeek) // Case #2 { length = -1; return false; } length = readStream.Length; // Case #3 } // We serialize header to a StringBuilder so that we can determine the length // following the pattern for HttpContent to try and determine the message length. // The perf overhead is no larger than for the other HttpContent implementations. byte[] header = SerializeHeader(); length += header.Length; return true; } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (disposing) { if (HttpRequestMessage != null) { HttpRequestMessage.Dispose(); HttpRequestMessage = null; } if (HttpResponseMessage != null) { HttpResponseMessage.Dispose(); HttpResponseMessage = null; } } base.Dispose(disposing); } /// /// Serializes the HTTP request line. /// /// Where to write the request line. /// The HTTP request. private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest) { Contract.Assert(message != null, "message cannot be null"); message.Append(httpRequest.Method + SP); message.Append(httpRequest.RequestUri.PathAndQuery + SP); message.Append(FormattingUtilities.HttpVersionToken + "/" + (httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1") + CRLF); // Only insert host header if not already present. if (httpRequest.Headers.Host == null) { message.Append(FormattingUtilities.HttpHostHeader + ColonSP + httpRequest.RequestUri.Authority + CRLF); } } /// /// Serializes the HTTP status line. /// /// Where to write the status line. /// The HTTP response. private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse) { Contract.Assert(message != null, "message cannot be null"); message.Append(FormattingUtilities.HttpVersionToken + "/" + (httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1") + SP); message.Append((int)httpResponse.StatusCode + SP); message.Append(httpResponse.ReasonPhrase + CRLF); } /// /// Serializes the header fields. /// /// Where to write the status line. /// The headers to write. private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers) { Contract.Assert(message != null, "message cannot be null"); if (headers != null) { foreach (KeyValuePair> header in headers) { if (_singleValueHeaderFields.Contains(header.Key)) { foreach (string value in header.Value) { message.Append(header.Key + ColonSP + value + CRLF); } } else if (_spaceSeparatedValueHeaderFields.Contains(header.Key)) { message.Append(header.Key + ColonSP + String.Join(SP, header.Value) + CRLF); } else { message.Append(header.Key + ColonSP + String.Join(CommaSeparator, header.Value) + CRLF); } } } } private byte[] SerializeHeader() { StringBuilder message = new StringBuilder(DefaultHeaderAllocation); HttpHeaders headers = null; HttpContent content = null; if (HttpRequestMessage != null) { SerializeRequestLine(message, HttpRequestMessage); headers = HttpRequestMessage.Headers; content = HttpRequestMessage.Content; } else { SerializeStatusLine(message, HttpResponseMessage); headers = HttpResponseMessage.Headers; content = HttpResponseMessage.Content; } SerializeHeaderFields(message, headers); if (content != null) { SerializeHeaderFields(message, content.Headers); } message.Append(CRLF); return Encoding.UTF8.GetBytes(message.ToString()); } private void ValidateStreamForReading(Stream stream) { // If the content needs to be written to a target stream a 2nd time, then the stream must support // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target // stream (e.g. a NetworkStream). if (_contentConsumed) { if (stream != null && stream.CanRead) { stream.Position = 0; } else { throw Error.InvalidOperation(Properties.Resources.HttpMessageContentAlreadyRead, FormattingUtilities.HttpContentType.Name, HttpRequestMessage != null ? FormattingUtilities.HttpRequestMessageType.Name : FormattingUtilities.HttpResponseMessageType.Name); } } _contentConsumed = true; } } }