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