//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Net.Sockets;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.Messaging.Reflection;
using Validation;
///
/// Manages sending direct messages to a remote party and receiving responses.
///
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable.")]
public abstract class Channel : IDisposable {
///
/// The encoding to use when writing out POST entity strings.
///
internal static readonly Encoding PostEntityEncoding = new UTF8Encoding(false);
///
/// A default set of XML dictionary reader quotas that are relatively safe from causing unbounded memory consumption.
///
internal static readonly XmlDictionaryReaderQuotas DefaultUntrustedXmlDictionaryReaderQuotas = new XmlDictionaryReaderQuotas {
MaxArrayLength = 1,
MaxDepth = 2,
MaxBytesPerRead = 8 * 1024,
MaxStringContentLength = 16 * 1024,
};
///
/// The content-type used on HTTP POST requests where the POST entity is a
/// URL-encoded series of key=value pairs.
///
protected internal const string HttpFormUrlEncoded = "application/x-www-form-urlencoded";
///
/// The content-type used for JSON serialized objects.
///
protected internal const string JsonEncoded = "application/json";
///
/// The "text/javascript" content-type that some servers return instead of the standard one.
///
protected internal const string JsonTextEncoded = "text/javascript";
///
/// The content-type for plain text.
///
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "PlainText", Justification = "Not 'Plaintext' in the crypographic sense.")]
protected internal const string PlainTextEncoded = "text/plain";
///
/// The content-type used on HTTP POST requests where the POST entity is a
/// URL-encoded series of key=value pairs.
/// This includes the character encoding.
///
protected internal static readonly ContentType HttpFormUrlEncodedContentType = new ContentType(HttpFormUrlEncoded) { CharSet = PostEntityEncoding.WebName };
///
/// The HTML that should be returned to the user agent as part of a 301 Redirect.
///
/// A string that should be used as the first argument to string.Format, where the {0} should be replaced with the URL to redirect to.
private const string RedirectResponseBodyFormat = @"
";
///
/// A list of binding elements in the order they must be applied to outgoing messages.
///
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List outgoingBindingElements = new List();
///
/// A list of binding elements in the order they must be applied to incoming messages.
///
private readonly List incomingBindingElements = new List();
///
/// The template for indirect messages that require form POST to forward through the user agent.
///
///
/// We are intentionally using " instead of the html single quote ' below because
/// the HtmlEncode'd values that we inject will only escape the double quote, so
/// only the double-quote used around these values is safe.
///
private const string IndirectMessageFormPostFormat = @"
";
///
/// The default cache of message descriptions to use unless they are customized.
///
///
/// This is a perf optimization, so that we don't reflect over every message type
/// every time a channel is constructed.
///
private static MessageDescriptionCollection defaultMessageDescriptions = new MessageDescriptionCollection();
///
/// A cache of reflected message types that may be sent or received on this channel.
///
private MessageDescriptionCollection messageDescriptions = defaultMessageDescriptions;
///
/// A tool that can figure out what kind of message is being received
/// so it can be deserialized.
///
private IMessageFactory messageTypeProvider;
///
/// Backing field for the property.
///
private int maximumIndirectMessageUrlLength = Configuration.DotNetOpenAuthSection.Messaging.MaximumIndirectMessageUrlLength;
///
/// Initializes a new instance of the class.
///
/// A class prepared to analyze incoming messages and indicate what concrete
/// message types can deserialize from it.
/// The binding elements to use in sending and receiving messages.
/// The order they are provided is used for outgoing messgaes, and reversed for incoming messages.
/// The host factories.
protected Channel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements, IHostFactories hostFactories) {
Requires.NotNull(messageTypeProvider, "messageTypeProvider");
Requires.NotNull(bindingElements, "bindingElements");
Requires.NotNull(hostFactories, "hostFactories");
this.messageTypeProvider = messageTypeProvider;
this.HostFactories = hostFactories;
this.XmlDictionaryReaderQuotas = DefaultUntrustedXmlDictionaryReaderQuotas;
this.outgoingBindingElements = new List(ValidateAndPrepareBindingElements(bindingElements));
this.incomingBindingElements = new List(this.outgoingBindingElements);
this.incomingBindingElements.Reverse();
foreach (var element in this.outgoingBindingElements) {
element.Channel = this;
}
}
///
/// An event fired whenever a message is about to be encoded and sent.
///
internal event EventHandler Sending;
///
/// Gets the host factories instance to use.
///
public IHostFactories HostFactories { get; private set; }
///
/// Gets or sets the maximum allowable size for a 301 Redirect response before we send
/// a 200 OK response with a scripted form POST with the parameters instead
/// in order to ensure successfully sending a large payload to another server
/// that might have a maximum allowable size restriction on its GET request.
///
/// The default value is 2048.
public int MaximumIndirectMessageUrlLength {
get {
return this.maximumIndirectMessageUrlLength;
}
set {
Requires.Range(value >= 500 && value <= 4096, "value");
this.maximumIndirectMessageUrlLength = value;
}
}
///
/// Gets or sets the message descriptions.
///
internal virtual MessageDescriptionCollection MessageDescriptions {
get {
return this.messageDescriptions;
}
set {
Requires.NotNull(value, "value");
this.messageDescriptions = value;
}
}
///
/// Gets a tool that can figure out what kind of message is being received
/// so it can be deserialized.
///
internal IMessageFactory MessageFactoryTestHook {
get { return this.MessageFactory; }
}
///
/// Gets or sets the outgoing message filter.
///
///
/// The outgoing message filter.
///
internal Action OutgoingMessageFilter { get; set; }
///
/// Gets or sets the incoming message filter.
///
///
/// The incoming message filter.
///
internal Action IncomingMessageFilter { get; set; }
///
/// Gets the binding elements used by this channel, in no particular guaranteed order.
///
protected internal ReadOnlyCollection BindingElements {
get {
var result = this.outgoingBindingElements.AsReadOnly();
Assumes.True(result != null); // should be an implicit BCL contract
return result;
}
}
///
/// Gets the binding elements used by this channel, in the order applied to outgoing messages.
///
protected internal ReadOnlyCollection OutgoingBindingElements {
get { return this.outgoingBindingElements.AsReadOnly(); }
}
///
/// Gets the binding elements used by this channel, in the order applied to incoming messages.
///
protected internal ReadOnlyCollection IncomingBindingElements {
get {
return this.incomingBindingElements.AsReadOnly();
}
}
///
/// Gets or sets a value indicating whether this instance is disposed.
///
///
/// true if this instance is disposed; otherwise, false.
///
protected internal bool IsDisposed { get; set; }
///
/// Gets or sets a tool that can figure out what kind of message is being received
/// so it can be deserialized.
///
protected virtual IMessageFactory MessageFactory {
get { return this.messageTypeProvider; }
set { this.messageTypeProvider = value; }
}
///
/// Gets or sets the XML dictionary reader quotas.
///
/// The XML dictionary reader quotas.
protected virtual XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas { get; set; }
///
/// Prepares an indirect message (either a request or response)
/// or direct message response for transmission to a remote party.
///
/// The one-way message to send
/// The cancellation token.
/// The pending user agent redirect based message to be sent as an HttpResponse.
public async Task PrepareResponseAsync(IProtocolMessage message, CancellationToken cancellationToken = default(CancellationToken)) {
Requires.NotNull(message, "message");
await this.ProcessOutgoingMessageAsync(message, cancellationToken);
Logger.Channel.DebugFormat("Sending message: {0}", message.GetType().Name);
HttpResponseMessage result;
switch (message.Transport) {
case MessageTransport.Direct:
// This is a response to a direct message.
result = this.PrepareDirectResponse(message);
break;
case MessageTransport.Indirect:
var directedMessage = message as IDirectedProtocolMessage;
ErrorUtilities.VerifyArgumentNamed(
directedMessage != null,
"message",
MessagingStrings.IndirectMessagesMustImplementIDirectedProtocolMessage,
typeof(IDirectedProtocolMessage).FullName);
ErrorUtilities.VerifyArgumentNamed(
directedMessage.Recipient != null,
"message",
MessagingStrings.DirectedMessageMissingRecipient);
result = this.PrepareIndirectResponse(directedMessage);
break;
default:
throw ErrorUtilities.ThrowArgumentNamed(
"message",
MessagingStrings.UnrecognizedEnumValue,
"Transport",
message.Transport);
}
// Apply caching policy to any response. We want to disable all caching because in auth* protocols,
// caching can be utilized in identity spoofing attacks.
result.Headers.CacheControl = new CacheControlHeaderValue {
NoCache = true,
NoStore = true,
MaxAge = TimeSpan.Zero,
MustRevalidate = true,
};
result.Headers.Pragma.Add(new NameValueHeaderValue("no-cache"));
return result;
}
///
/// Gets the protocol message embedded in the given HTTP request, if present.
///
/// The expected type of the message to be received.
/// The request to search for an embedded message.
/// The cancellation token.
///
/// True if the expected message was recognized and deserialized. False otherwise.
///
/// Thrown when is null.
/// Thrown when a request message of an unexpected type is received.
public async Task TryReadFromRequestAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken)
where TRequest : class, IProtocolMessage {
Requires.NotNull(httpRequest, "httpRequest");
IProtocolMessage untypedRequest = await this.ReadFromRequestAsync(httpRequest, cancellationToken);
if (untypedRequest == null) {
return null;
}
var request = untypedRequest as TRequest;
ErrorUtilities.VerifyProtocol(request != null, MessagingStrings.UnexpectedMessageReceived, typeof(TRequest), untypedRequest.GetType());
return request;
}
///
/// Gets the protocol message embedded in the given HTTP request.
///
/// The expected type of the message to be received.
/// The request to search for an embedded message.
/// The cancellation token.
///
/// The deserialized message. Never null.
///
/// Thrown if the expected message was not recognized in the response.
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")]
public async Task ReadFromRequestAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken)
where TRequest : class, IProtocolMessage {
Requires.NotNull(httpRequest, "httpRequest");
TRequest request = await this.TryReadFromRequestAsync(httpRequest, cancellationToken);
ErrorUtilities.VerifyProtocol(request != null, MessagingStrings.ExpectedMessageNotReceived, typeof(TRequest));
return request;
}
///
/// Gets the protocol message that may be embedded in the given HTTP request.
///
/// The request to search for an embedded message.
/// The cancellation token.
///
/// The deserialized message, if one is found. Null otherwise.
///
public async Task ReadFromRequestAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken) {
Requires.NotNull(httpRequest, "httpRequest");
if (Logger.Channel.IsInfoEnabled() && httpRequest.RequestUri != null) {
Logger.Channel.InfoFormat("Scanning incoming request for messages: {0}", httpRequest.RequestUri.AbsoluteUri);
}
IDirectedProtocolMessage requestMessage = await this.ReadFromRequestCoreAsync(httpRequest, cancellationToken);
if (requestMessage != null) {
Logger.Channel.DebugFormat("Incoming request received: {0}", requestMessage.GetType().Name);
var directRequest = requestMessage as IHttpDirectRequest;
if (directRequest != null) {
foreach (var header in httpRequest.Headers) {
directRequest.Headers.Add(header.Key, header.Value);
}
}
await this.ProcessIncomingMessageAsync(requestMessage, cancellationToken);
}
return requestMessage;
}
///
/// Sends a direct message to a remote party and waits for the response.
///
/// The expected type of the message to be received.
/// The message to send.
/// The cancellation token.
///
/// The remote party's response.
///
/// Thrown if no message is recognized in the response
/// or an unexpected type of message is received.
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")]
public async Task RequestAsync(IDirectedProtocolMessage requestMessage, CancellationToken cancellationToken)
where TResponse : class, IProtocolMessage {
Requires.NotNull(requestMessage, "requestMessage");
IProtocolMessage response = await this.RequestAsync(requestMessage, cancellationToken);
ErrorUtilities.VerifyProtocol(response != null, MessagingStrings.ExpectedMessageNotReceived, typeof(TResponse));
var expectedResponse = response as TResponse;
ErrorUtilities.VerifyProtocol(expectedResponse != null, MessagingStrings.UnexpectedMessageReceived, typeof(TResponse), response.GetType());
return expectedResponse;
}
///
/// Sends a direct message to a remote party and waits for the response.
///
/// The message to send.
/// The cancellation token.
///
/// The remote party's response. Guaranteed to never be null.
///
/// Thrown if the response does not include a protocol message.
public async Task RequestAsync(IDirectedProtocolMessage requestMessage, CancellationToken cancellationToken) {
Requires.NotNull(requestMessage, "requestMessage");
await this.ProcessOutgoingMessageAsync(requestMessage, cancellationToken);
Logger.Channel.DebugFormat("Sending {0} request.", requestMessage.GetType().Name);
var responseMessage = await this.RequestCoreAsync(requestMessage, cancellationToken);
ErrorUtilities.VerifyProtocol(responseMessage != null, MessagingStrings.ExpectedMessageNotReceived, typeof(IProtocolMessage).Name);
Logger.Channel.DebugFormat("Received {0} response.", responseMessage.GetType().Name);
await this.ProcessIncomingMessageAsync(responseMessage, cancellationToken);
return responseMessage;
}
#region IDisposable Members
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
///
/// Verifies the integrity and applicability of an incoming message.
///
/// The message just received.
/// The cancellation token.
///
/// A task that completes with the asynchronous operation.
///
/// Thrown when the message is somehow invalid.
/// This can be due to tampering, replay attack or expiration, among other things.
internal Task ProcessIncomingMessageTestHookAsync(IProtocolMessage message, CancellationToken cancellationToken) {
return this.ProcessIncomingMessageAsync(message, cancellationToken);
}
///
/// Prepares an HTTP request that carries a given message.
///
/// The message to send.
/// The prepared to send the request.
///
/// This method must be overridden by a derived class, unless the method
/// is overridden and does not require this method.
///
internal HttpRequestMessage CreateHttpRequestTestHook(IDirectedProtocolMessage request) {
return this.CreateHttpRequest(request);
}
///
/// Queues a message for sending in the response stream where the fields
/// are sent in the response stream in querystring style.
///
/// The message to send as a response.
/// The pending user agent redirect based message to be sent as an HttpResponse.
///
/// This method implements spec OAuth V1.0 section 5.3.
///
internal HttpResponseMessage PrepareDirectResponseTestHook(IProtocolMessage response) {
return this.PrepareDirectResponse(response);
}
///
/// Gets the protocol message that may be in the given HTTP response.
///
/// The response that is anticipated to contain an protocol message.
/// The cancellation token.
///
/// The deserialized message parts, if found. Null otherwise.
///
/// Thrown when the response is not valid.
internal Task> ReadFromResponseCoreAsyncTestHook(HttpResponseMessage response, CancellationToken cancellationToken) {
return this.ReadFromResponseCoreAsync(response, cancellationToken);
}
///
/// Prepares a message for transmit by applying signatures, nonces, etc.
///
/// The message to prepare for sending.
/// The cancellation token.
///
/// A task that completes with the asynchronous operation.
///
///
/// This method should NOT be called by derived types
/// except when sending ONE WAY request messages.
///
internal Task ProcessOutgoingMessageTestHookAsync(IProtocolMessage message, CancellationToken cancellationToken = default(CancellationToken)) {
return this.ProcessOutgoingMessageAsync(message, cancellationToken);
}
///
/// Parses the URL encoded form content.
///
/// The request.
/// The cancellation token.
/// A sequence of key=value pairs found in the request's entity; or an empty sequence if none are found.
protected internal static async Task>> ParseUrlEncodedFormContentAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
if (request.Content != null && request.Content.Headers.ContentType != null
&& request.Content.Headers.ContentType.MediaType.Equals(HttpFormUrlEncoded)) {
return HttpUtility.ParseQueryString(await request.Content.ReadAsStringAsync()).AsKeyValuePairs();
}
return Enumerable.Empty>();
}
///
/// Gets the HTTP context for the current HTTP request.
///
/// An HttpContextBase instance.
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Allocates memory")]
protected internal virtual HttpContextBase GetHttpContext() {
RequiresEx.ValidState(HttpContext.Current != null, MessagingStrings.HttpContextRequired);
return new HttpContextWrapper(HttpContext.Current);
}
///
/// Gets the current HTTP request being processed.
///
/// The HttpRequestInfo for the current request.
///
/// Requires an context.
///
/// Thrown if HttpContext.Current == null.
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly call should not be a property.")]
protected internal virtual HttpRequestBase GetRequestFromContext() {
RequiresEx.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
Assumes.True(HttpContext.Current.Request.Url != null);
Assumes.True(HttpContext.Current.Request.RawUrl != null);
return new HttpRequestWrapper(HttpContext.Current.Request);
}
///
/// Adds just the binary data part of a message to a multipart form content object.
///
/// The request message with binary data.
/// The initialized HttpContent.
protected static MultipartFormDataContent InitializeMultipartFormDataContent(IMessageWithBinaryData requestMessageWithBinaryData) {
Requires.NotNull(requestMessageWithBinaryData, "requestMessageWithBinaryData");
var content = new MultipartFormDataContent();
foreach (var part in requestMessageWithBinaryData.BinaryData) {
if (string.IsNullOrEmpty(part.Name)) {
content.Add(part.Content);
} else if (string.IsNullOrEmpty(part.FileName)) {
content.Add(part.Content, part.Name);
} else {
content.Add(part.Content, part.Name, part.FileName);
}
}
return content;
}
///
/// Checks whether a given HTTP method is expected to include an entity body in its request.
///
/// The HTTP method.
/// true if the HTTP method is supposed to have an entity; false otherwise.
protected static bool HttpMethodHasEntity(HttpMethod httpMethod) {
Requires.NotNull(httpMethod, "httpMethod");
if (httpMethod == HttpMethod.Get ||
httpMethod == HttpMethod.Head ||
httpMethod == HttpMethod.Delete ||
httpMethod == HttpMethod.Options) {
return false;
} else if (httpMethod == HttpMethod.Post ||
httpMethod == HttpMethod.Put ||
string.Equals(httpMethod.Method, "PATCH", StringComparison.Ordinal)) {
return true;
} else {
throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod);
}
}
///
/// Applies message prescribed HTTP response headers to an outgoing web response.
///
/// The message.
/// The HTTP response.
protected static void ApplyMessageTemplate(IMessage message, HttpResponseMessage response) {
Requires.NotNull(message, "message");
var httpMessage = message as IHttpDirectResponse;
if (httpMessage != null) {
response.StatusCode = httpMessage.HttpStatusCode;
foreach (string headerName in httpMessage.Headers) {
response.Headers.Add(headerName, httpMessage.Headers[headerName]);
}
}
}
///
/// Releases unmanaged and - optionally - managed resources
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing) {
if (disposing) {
// Call dispose on any binding elements that need it.
foreach (IDisposable bindingElement in this.BindingElements.OfType()) {
bindingElement.Dispose();
}
this.IsDisposed = true;
}
}
///
/// Fires the event.
///
/// The message about to be encoded and sent.
protected virtual void OnSending(IProtocolMessage message) {
Requires.NotNull(message, "message");
var sending = this.Sending;
if (sending != null) {
sending(this, new ChannelEventArgs(message));
}
}
///
/// Submits a direct request message to some remote party and blocks waiting for an immediately reply.
///
/// The request message.
/// The cancellation token.
/// The response message, or null if the response did not carry a message.
///
/// Typically a deriving channel will override to customize this method's
/// behavior. However in non-HTTP frameworks, such as unit test mocks, it may be appropriate to override
/// this method to eliminate all use of an HTTP transport.
///
protected virtual async Task RequestCoreAsync(IDirectedProtocolMessage request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
Requires.That(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
if (this.OutgoingMessageFilter != null) {
this.OutgoingMessageFilter(request);
}
var webRequest = this.CreateHttpRequest(request);
var directRequest = request as IHttpDirectRequest;
if (directRequest != null) {
foreach (var header in directRequest.Headers) {
webRequest.Headers.Add(header.Key, header.Value);
}
}
try {
using (var httpClient = this.HostFactories.CreateHttpClient()) {
using (var response = await httpClient.SendAsync(webRequest, cancellationToken)) {
if (response.Content != null) {
var responseFields = await this.ReadFromResponseCoreAsync(response, cancellationToken);
if (responseFields != null) {
var responseMessage = this.MessageFactory.GetNewResponseMessage(request, responseFields);
if (responseMessage != null) {
this.OnReceivingDirectResponse(response, responseMessage);
var messageAccessor = this.MessageDescriptions.GetAccessor(responseMessage);
messageAccessor.Deserialize(responseFields);
return responseMessage;
}
}
}
if (!response.IsSuccessStatusCode) {
var errorContent = (response.Content != null) ? await response.Content.ReadAsStringAsync() : null;
Logger.Http.ErrorFormat(
"Error received in HTTP response: {0} {1}\n{2}", (int)response.StatusCode, response.ReasonPhrase, errorContent);
response.EnsureSuccessStatusCode(); // throw so we can wrap it in our catch block.
}
return null;
}
}
} catch (HttpRequestException requestException) {
throw ErrorUtilities.Wrap(requestException, "Error sending HTTP request or receiving response.");
}
}
///
/// Called when receiving a direct response message, before deserialization begins.
///
/// The HTTP direct response.
/// The newly instantiated message, prior to deserialization.
protected virtual void OnReceivingDirectResponse(HttpResponseMessage response, IDirectResponseProtocolMessage message) {
}
///
/// Gets the protocol message that may be embedded in the given HTTP request.
///
/// The request to search for an embedded message.
/// The cancellation token.
/// The deserialized message, if one is found. Null otherwise.
protected virtual async Task ReadFromRequestCoreAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
Logger.Channel.DebugFormat("Incoming HTTP request: {0} {1}", request.Method, request.RequestUri.AbsoluteUri);
var fields = new Dictionary();
// Search Form data first, and if nothing is there search the QueryString
fields.AddRange(await ParseUrlEncodedFormContentAsync(request, cancellationToken));
if (fields.Count == 0 && request.Method.Method != "POST") { // OpenID 2.0 section 4.1.2
fields.AddRange(HttpUtility.ParseQueryString(request.RequestUri.Query).AsKeyValuePairs());
}
MessageReceivingEndpoint recipient;
try {
recipient = request.GetRecipient();
} catch (ArgumentException ex) {
Logger.Messaging.WarnFormat("Unrecognized HTTP request: {0}", ex);
return null;
}
return (IDirectedProtocolMessage)this.Receive(fields, recipient);
}
///
/// Deserializes a dictionary of values into a message.
///
/// The dictionary of values that were read from an HTTP request or response.
/// Information about where the message was directed. Null for direct response messages.
/// The deserialized message, or null if no message could be recognized in the provided data.
protected virtual IProtocolMessage Receive(Dictionary fields, MessageReceivingEndpoint recipient) {
Requires.NotNull(fields, "fields");
this.FilterReceivedFields(fields);
IProtocolMessage message = this.MessageFactory.GetNewRequestMessage(recipient, fields);
// If there was no data, or we couldn't recognize it as a message, abort.
if (message == null) {
return null;
}
// Ensure that the message came in using an allowed HTTP verb for this message type.
var directedMessage = message as IDirectedProtocolMessage;
ErrorUtilities.VerifyProtocol(recipient == null || (directedMessage != null && (recipient.AllowedMethods & directedMessage.HttpMethods) != 0), MessagingStrings.UnsupportedHttpVerbForMessageType, message.GetType().Name, recipient.AllowedMethods);
// We have a message! Assemble it.
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
messageAccessor.Deserialize(fields);
return message;
}
///
/// Queues an indirect message for transmittal via the user agent.
///
/// The message to send.
/// The pending user agent redirect based message to be sent as an HttpResponse.
protected virtual HttpResponseMessage PrepareIndirectResponse(IDirectedProtocolMessage message) {
Requires.NotNull(message, "message");
Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.That((message.HttpMethods & (HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest)) != 0, "message", "GET or POST expected.");
Assumes.True(message != null && message.Recipient != null);
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
Assumes.True(message != null && message.Recipient != null);
var fields = messageAccessor.Serialize();
HttpResponseMessage response = null;
bool tooLargeForGet = false;
if ((message.HttpMethods & HttpDeliveryMethods.GetRequest) == HttpDeliveryMethods.GetRequest) {
bool payloadInFragment = false;
var httpIndirect = message as IHttpIndirectResponse;
if (httpIndirect != null) {
payloadInFragment = httpIndirect.Include301RedirectPayloadInFragment;
}
// First try creating a 301 redirect, and fallback to a form POST
// if the message is too big.
response = this.Create301RedirectResponse(message, fields, payloadInFragment);
tooLargeForGet = response.Headers.Location.PathAndQuery.Length > this.MaximumIndirectMessageUrlLength;
}
// Make sure that if the message is too large for GET that POST is allowed.
if (tooLargeForGet) {
ErrorUtilities.VerifyProtocol(
(message.HttpMethods & HttpDeliveryMethods.PostRequest) == HttpDeliveryMethods.PostRequest,
MessagingStrings.MessageExceedsGetSizePostNotAllowed);
}
// If GET didn't work out, for whatever reason...
if (response == null || tooLargeForGet) {
response = this.CreateFormPostResponse(message, fields);
}
return response;
}
///
/// Encodes an HTTP response that will instruct the user agent to forward a message to
/// some remote third party using a 301 Redirect GET method.
///
/// The message to forward.
/// The pre-serialized fields from the message.
/// if set to true the redirect will contain the message payload in the #fragment portion of the URL rather than the ?querystring.
/// The encoded HTTP response.
[Pure]
protected virtual HttpResponseMessage Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary fields, bool payloadInFragment = false) {
Requires.NotNull(message, "message");
Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.NotNull(fields, "fields");
// As part of this redirect, we include an HTML body in order to get passed some proxy filters
// such as WebSense.
UriBuilder builder = new UriBuilder(message.Recipient);
if (payloadInFragment) {
builder.AppendFragmentArgs(fields);
} else {
builder.AppendQueryArgs(fields);
}
Logger.Http.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri);
HttpResponseMessage response = new HttpResponseMessageWithOriginal(message) {
StatusCode = HttpStatusCode.Redirect,
Content = new StringContent(string.Format(CultureInfo.InvariantCulture, RedirectResponseBodyFormat, builder.Uri.AbsoluteUri)),
};
response.Headers.Location = builder.Uri;
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html") { CharSet = "utf-8" };
return response;
}
///
/// Encodes an HTTP response that will instruct the user agent to forward a message to
/// some remote third party using a form POST method.
///
/// The message to forward.
/// The pre-serialized fields from the message.
/// The encoded HTTP response.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
protected virtual HttpResponseMessage CreateFormPostResponse(IDirectedProtocolMessage message, IDictionary fields) {
Requires.NotNull(message, "message");
Requires.That(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.NotNull(fields, "fields");
using (StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture)) {
StringBuilder hiddenFields = new StringBuilder();
foreach (var field in fields) {
hiddenFields.AppendFormat(
"\t\r\n",
HttpUtility.HtmlEncode(field.Key),
HttpUtility.HtmlEncode(field.Value));
}
bodyWriter.WriteLine(
IndirectMessageFormPostFormat,
HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri),
hiddenFields);
bodyWriter.Flush();
HttpResponseMessage response = new HttpResponseMessageWithOriginal(message) {
StatusCode = HttpStatusCode.OK,
Content = new StringContent(bodyWriter.ToString()),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
return response;
}
}
///
/// Gets the protocol message that may be in the given HTTP response.
///
/// The response that is anticipated to contain an protocol message.
/// The cancellation token.
///
/// The deserialized message parts, if found. Null otherwise.
///
/// Thrown when the response is not valid.
protected abstract Task> ReadFromResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken);
///
/// Prepares an HTTP request that carries a given message.
///
/// The message to send.
/// The prepared to send the request.
///
/// This method must be overridden by a derived class, unless the method
/// is overridden and does not require this method.
///
protected virtual HttpRequestMessage CreateHttpRequest(IDirectedProtocolMessage request) {
Requires.NotNull(request, "request");
Requires.That(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
throw new NotImplementedException();
}
///
/// Queues a message for sending in the response stream where the fields
/// are sent in the response stream in querystring style.
///
/// The message to send as a response.
/// The pending user agent redirect based message to be sent as an HttpResponse.
///
/// This method implements spec OAuth V1.0 section 5.3.
///
protected abstract HttpResponseMessage PrepareDirectResponse(IProtocolMessage response);
///
/// Serializes the given message as a JSON string.
///
/// The message to serialize.
/// A JSON string.
[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")]
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
protected virtual string SerializeAsJson(IMessage message) {
Requires.NotNull(message, "message");
return MessagingUtilities.SerializeAsJson(message, this.MessageDescriptions);
}
///
/// Deserializes from flat data from a JSON object.
///
/// A JSON string.
/// The simple "key":"value" pairs from a JSON-encoded object.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
protected virtual IDictionary DeserializeFromJson(string json) {
Requires.NotNullOrEmpty(json, "json");
var dictionary = new Dictionary();
using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(json), this.XmlDictionaryReaderQuotas)) {
MessageSerializer.DeserializeJsonAsFlatDictionary(dictionary, jsonReader);
}
return dictionary;
}
///
/// Prepares a message for transmit by applying signatures, nonces, etc.
///
/// The message to prepare for sending.
/// The cancellation token.
///
/// A task that completes with the asynchronous operation.
///
/// Thrown if the message does not have the minimal required protections applied.
///
/// This method should NOT be called by derived types
/// except when sending ONE WAY request messages.
///
protected async Task ProcessOutgoingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) {
Requires.NotNull(message, "message");
Logger.Channel.DebugFormat("Preparing to send {0} ({1}) message.", message.GetType().Name, message.Version);
this.OnSending(message);
// Give the message a chance to do custom serialization.
IMessageWithEvents eventedMessage = message as IMessageWithEvents;
if (eventedMessage != null) {
eventedMessage.OnSending();
}
MessageProtections appliedProtection = MessageProtections.None;
foreach (IChannelBindingElement bindingElement in this.outgoingBindingElements) {
Assumes.True(bindingElement.Channel != null);
MessageProtections? elementProtection = await bindingElement.ProcessOutgoingMessageAsync(message, cancellationToken);
if (elementProtection.HasValue) {
Logger.Bindings.DebugFormat("Binding element {0} applied to message.", bindingElement.GetType().FullName);
// Ensure that only one protection binding element applies to this message
// for each protection type.
ErrorUtilities.VerifyProtocol((appliedProtection & elementProtection.Value) == 0, MessagingStrings.TooManyBindingsOfferingSameProtection, elementProtection.Value);
appliedProtection |= elementProtection.Value;
} else {
Logger.Bindings.DebugFormat("Binding element {0} did not apply to message.", bindingElement.GetType().FullName);
}
}
// Ensure that the message's protection requirements have been satisfied.
if ((message.RequiredProtection & appliedProtection) != message.RequiredProtection) {
throw new UnprotectedMessageException(message, appliedProtection);
}
this.EnsureValidMessageParts(message);
message.EnsureValidMessage();
if (this.OutgoingMessageFilter != null) {
this.OutgoingMessageFilter(message);
}
if (Logger.Channel.IsInfoEnabled()) {
var directedMessage = message as IDirectedProtocolMessage;
string recipient = (directedMessage != null && directedMessage.Recipient != null) ? directedMessage.Recipient.AbsoluteUri : "";
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
Logger.Channel.InfoFormat(
"Prepared outgoing {0} ({1}) message for {2}: {3}{4}",
message.GetType().Name,
message.Version,
recipient,
Environment.NewLine,
messageAccessor.ToStringDeferred());
}
}
///
/// Prepares to send a request to the Service Provider as the query string in a GET request.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method is simply a standard HTTP Get request with the message parts serialized to the query string.
/// This method satisfies OAuth 1.0 section 5.2, item #3.
///
protected virtual HttpRequestMessage InitializeRequestAsGet(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Requires.That(requestMessage.Recipient != null, "requestMessage", MessagingStrings.DirectedMessageMissingRecipient);
var messageAccessor = this.MessageDescriptions.GetAccessor(requestMessage);
var fields = messageAccessor.Serialize();
UriBuilder builder = new UriBuilder(requestMessage.Recipient);
MessagingUtilities.AppendQueryArgs(builder, fields);
var httpRequest = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
this.PrepareHttpWebRequest(httpRequest);
return httpRequest;
}
///
/// Prepares to send a request to the Service Provider as the query string in a HEAD request.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method is simply a standard HTTP HEAD request with the message parts serialized to the query string.
/// This method satisfies OAuth 1.0 section 5.2, item #3.
///
protected virtual HttpRequestMessage InitializeRequestAsHead(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Requires.That(requestMessage.Recipient != null, "requestMessage", MessagingStrings.DirectedMessageMissingRecipient);
var request = this.InitializeRequestAsGet(requestMessage);
request.Method = HttpMethod.Head;
return request;
}
///
/// Prepares to send a request to the Service Provider as the payload of a POST request.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method is simply a standard HTTP POST request with the message parts serialized to the POST entity
/// with the application/x-www-form-urlencoded content type
/// This method satisfies OAuth 1.0 section 5.2, item #2 and OpenID 2.0 section 4.1.2.
///
protected virtual HttpRequestMessage InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
var messageAccessor = this.MessageDescriptions.GetAccessor(requestMessage);
var fields = messageAccessor.Serialize();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestMessage.Recipient);
this.PrepareHttpWebRequest(httpRequest);
var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData;
if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) {
var content = InitializeMultipartFormDataContent(requestMessageWithBinaryData);
// When sending multi-part, all data gets send as multi-part -- even the non-binary data.
foreach (var field in fields) {
content.Add(new StringContent(field.Value), field.Key);
}
httpRequest.Content = content;
} else {
ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart);
httpRequest.Content = new FormUrlEncodedContent(fields);
}
return httpRequest;
}
///
/// Prepares to send a request to the Service Provider as the query string in a PUT request.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method is simply a standard HTTP PUT request with the message parts serialized to the query string.
///
protected virtual HttpRequestMessage InitializeRequestAsPut(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
var request = this.InitializeRequestAsGet(requestMessage);
request.Method = HttpMethod.Put;
return request;
}
///
/// Prepares to send a request to the Service Provider as the query string in a DELETE request.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method is simply a standard HTTP DELETE request with the message parts serialized to the query string.
///
protected virtual HttpRequestMessage InitializeRequestAsDelete(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
var request = this.InitializeRequestAsGet(requestMessage);
request.Method = HttpMethod.Delete;
return request;
}
///
/// Verifies the integrity and applicability of an incoming message.
///
/// The message just received.
/// The cancellation token.
///
/// A task that completes with the asynchronous operation.
///
/// Thrown if the message does not have the minimal required protections applied.
/// Thrown when the message is somehow invalid.
/// This can be due to tampering, replay attack or expiration, among other things.
protected virtual async Task ProcessIncomingMessageAsync(IProtocolMessage message, CancellationToken cancellationToken) {
Requires.NotNull(message, "message");
if (Logger.Channel.IsInfoEnabled()) {
var messageAccessor = this.MessageDescriptions.GetAccessor(message, true);
Logger.Channel.InfoFormat(
"Processing incoming {0} ({1}) message:{2}{3}",
message.GetType().Name,
message.Version,
Environment.NewLine,
messageAccessor.ToStringDeferred());
}
if (this.IncomingMessageFilter != null) {
this.IncomingMessageFilter(message);
}
MessageProtections appliedProtection = MessageProtections.None;
foreach (IChannelBindingElement bindingElement in this.IncomingBindingElements) {
Assumes.True(bindingElement.Channel != null); // CC bug: this.IncomingBindingElements ensures this... why must we assume it here?
MessageProtections? elementProtection = await bindingElement.ProcessIncomingMessageAsync(message, cancellationToken);
if (elementProtection.HasValue) {
Logger.Bindings.DebugFormat("Binding element {0} applied to message.", bindingElement.GetType().FullName);
// Ensure that only one protection binding element applies to this message
// for each protection type.
if ((appliedProtection & elementProtection.Value) != 0) {
// It turns out that this MAY not be a fatal error condition.
// But it may indicate a problem.
// Specifically, when this RP uses OpenID 1.x to talk to an OP, and both invent
// their own replay protection for OpenID 1.x, and the OP happens to reuse
// openid.response_nonce, then this RP may consider both the RP's own nonce and
// the OP's nonce and "apply" replay protection twice. This actually isn't a problem.
Logger.Bindings.WarnFormat(MessagingStrings.TooManyBindingsOfferingSameProtection, elementProtection.Value);
}
appliedProtection |= elementProtection.Value;
} else {
Logger.Bindings.DebugFormat("Binding element {0} did not apply to message.", bindingElement.GetType().FullName);
}
}
// Ensure that the message's protection requirements have been satisfied.
if ((message.RequiredProtection & appliedProtection) != message.RequiredProtection) {
throw new UnprotectedMessageException(message, appliedProtection);
}
// Give the message a chance to do custom serialization.
IMessageWithEvents eventedMessage = message as IMessageWithEvents;
if (eventedMessage != null) {
eventedMessage.OnReceiving();
}
if (Logger.Channel.IsDebugEnabled()) {
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
Logger.Channel.DebugFormat(
"After binding element processing, the received {0} ({1}) message is: {2}{3}",
message.GetType().Name,
message.Version,
Environment.NewLine,
messageAccessor.ToStringDeferred());
}
// We do NOT verify that all required message parts are present here... the
// message deserializer did for us. It would be too late to do it here since
// they might look initialized by the time we have an IProtocolMessage instance.
message.EnsureValidMessage();
}
///
/// Allows preprocessing and validation of message data before an appropriate message type is
/// selected or deserialized.
///
/// The received message data.
protected virtual void FilterReceivedFields(IDictionary fields) {
}
///
/// Performs additional processing on an outgoing web request before it is sent to the remote server.
///
/// The request.
protected virtual void PrepareHttpWebRequest(HttpRequestMessage request) {
Requires.NotNull(request, "request");
}
///
/// Customizes the binding element order for outgoing and incoming messages.
///
/// The outgoing order.
/// The incoming order.
///
/// No binding elements can be added or removed from the channel using this method.
/// Only a customized order is allowed.
///
/// Thrown if a binding element is new or missing in one of the ordered lists.
protected void CustomizeBindingElementOrder(IEnumerable outgoingOrder, IEnumerable incomingOrder) {
Requires.NotNull(outgoingOrder, "outgoingOrder");
Requires.NotNull(incomingOrder, "incomingOrder");
ErrorUtilities.VerifyArgument(this.IsBindingElementOrderValid(outgoingOrder), MessagingStrings.InvalidCustomBindingElementOrder);
ErrorUtilities.VerifyArgument(this.IsBindingElementOrderValid(incomingOrder), MessagingStrings.InvalidCustomBindingElementOrder);
this.outgoingBindingElements.Clear();
this.outgoingBindingElements.AddRange(outgoingOrder);
this.incomingBindingElements.Clear();
this.incomingBindingElements.AddRange(incomingOrder);
}
///
/// Ensures a consistent and secure set of binding elements and
/// sorts them as necessary for a valid sequence of operations.
///
/// The binding elements provided to the channel.
/// The properly ordered list of elements.
/// Thrown when the binding elements are incomplete or inconsistent with each other.
private static IEnumerable ValidateAndPrepareBindingElements(IEnumerable elements) {
Requires.NullOrNotNullElements(elements, "elements");
if (elements == null) {
return new IChannelBindingElement[0];
}
// Filter the elements between the mere transforming ones and the protection ones.
var transformationElements = new List(
elements.Where(element => element.Protection == MessageProtections.None));
var protectionElements = new List(
elements.Where(element => element.Protection != MessageProtections.None));
bool wasLastProtectionPresent = true;
foreach (MessageProtections protectionKind in Enum.GetValues(typeof(MessageProtections))) {
if (protectionKind == MessageProtections.None) {
continue;
}
int countProtectionsOfThisKind = protectionElements.Count(element => (element.Protection & protectionKind) == protectionKind);
// Each protection binding element is backed by the presence of its dependent protection(s).
ErrorUtilities.VerifyProtocol(!(countProtectionsOfThisKind > 0 && !wasLastProtectionPresent), MessagingStrings.RequiredProtectionMissing, protectionKind);
wasLastProtectionPresent = countProtectionsOfThisKind > 0;
}
// Put the binding elements in order so they are correctly applied to outgoing messages.
// Start with the transforming (non-protecting) binding elements first and preserve their original order.
var orderedList = new List(transformationElements);
// Now sort the protection binding elements among themselves and add them to the list.
orderedList.AddRange(protectionElements.OrderBy(element => element.Protection, BindingElementOutgoingMessageApplicationOrder));
return orderedList.AsEnumerable();
}
///
/// Puts binding elements in their correct outgoing message processing order.
///
/// The first protection type to compare.
/// The second protection type to compare.
///
/// -1 if should be applied to an outgoing message before .
/// 1 if should be applied to an outgoing message before .
/// 0 if it doesn't matter.
///
private static int BindingElementOutgoingMessageApplicationOrder(MessageProtections protection1, MessageProtections protection2) {
ErrorUtilities.VerifyInternal(protection1 != MessageProtections.None || protection2 != MessageProtections.None, "This comparison function should only be used to compare protection binding elements. Otherwise we change the order of user-defined message transformations.");
// Now put the protection ones in the right order.
return -((int)protection1).CompareTo((int)protection2); // descending flag ordinal order
}
///
/// Verifies that all required message parts are initialized to values
/// prior to sending the message to a remote party.
///
/// The message to verify.
///
/// Thrown when any required message part does not have a value.
///
private void EnsureValidMessageParts(IProtocolMessage message) {
Requires.NotNull(message, "message");
MessageDictionary dictionary = this.MessageDescriptions.GetAccessor(message);
MessageDescription description = this.MessageDescriptions.Get(message);
description.EnsureMessagePartsPassBasicValidation(dictionary);
}
///
/// Determines whether a given ordered list of binding elements includes every
/// binding element in this channel exactly once.
///
/// The list of binding elements to test.
///
/// true if the given list is a valid description of a binding element ordering; otherwise, false.
///
[Pure]
private bool IsBindingElementOrderValid(IEnumerable order) {
Requires.NotNull(order, "order");
// Check that the same number of binding elements are defined.
if (order.Count() != this.OutgoingBindingElements.Count) {
return false;
}
// Check that every binding element appears exactly once.
if (order.Any(el => !this.OutgoingBindingElements.Contains(el))) {
return false;
}
return true;
}
}
}