//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. 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.Mime;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;
using System.Web;
using System.Xml;
using DotNetOpenAuth.Messaging.Reflection;
///
/// Manages sending direct messages to a remote party and receiving responses.
///
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable.")]
[ContractVerification(true)]
[ContractClass(typeof(ChannelContract))]
public abstract class Channel : IDisposable {
///
/// The encoding to use when writing out POST entity strings.
///
internal static readonly Encoding PostEntityEncoding = new UTF8Encoding(false);
///
/// 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 store for the property.
///
private RequestCachePolicy cachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
///
/// 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.
protected Channel(IMessageFactory messageTypeProvider, params IChannelBindingElement[] bindingElements) {
Requires.NotNull(messageTypeProvider, "messageTypeProvider");
this.messageTypeProvider = messageTypeProvider;
this.WebRequestHandler = new StandardWebRequestHandler();
this.XmlDictionaryReaderQuotas = new XmlDictionaryReaderQuotas {
MaxArrayLength = 1,
MaxDepth = 2,
MaxBytesPerRead = 8 * 1024,
MaxStringContentLength = 16 * 1024,
};
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 or sets an instance to a that will be used when
/// submitting HTTP requests and waiting for responses.
///
///
/// This defaults to a straightforward implementation, but can be set
/// to a mock object for testing purposes.
///
public IDirectWebRequestHandler WebRequestHandler { get; 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.InRange(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 the binding elements used by this channel, in no particular guaranteed order.
///
protected internal ReadOnlyCollection BindingElements {
get {
Contract.Ensures(Contract.Result>() != null);
var result = this.outgoingBindingElements.AsReadOnly();
Contract.Assume(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 {
Contract.Ensures(Contract.Result>().All(be => be.Channel != null));
Contract.Ensures(Contract.Result>().All(be => be != null));
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 cache policy to use for direct message requests.
///
/// Default is .
protected RequestCachePolicy CachePolicy {
get {
return this.cachePolicy;
}
set {
Requires.NotNull(value, "value");
this.cachePolicy = value;
}
}
///
/// Gets or sets the XML dictionary reader quotas.
///
/// The XML dictionary reader quotas.
protected virtual XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas { get; set; }
///
/// Sends an indirect message (either a request or response)
/// or direct message response for transmission to a remote party
/// and ends execution on the current page or handler.
///
/// The one-way message to send
/// Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.
///
/// Requires an HttpContext.Current context.
///
[EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use the Respond method instead, and prepare for execution to continue on this page beyond the call to Respond.")]
public void Send(IProtocolMessage message) {
Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
Requires.NotNull(message, "message");
this.PrepareResponse(message).Respond(HttpContext.Current, true);
}
///
/// Sends an indirect message (either a request or response)
/// or direct message response for transmission to a remote party
/// and skips most of the remaining ASP.NET request handling pipeline.
///
/// The one-way message to send
///
/// Requires an HttpContext.Current context.
///
public void Respond(IProtocolMessage message) {
Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
Requires.NotNull(message, "message");
this.PrepareResponse(message).Respond();
}
///
/// 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 pending user agent redirect based message to be sent as an HttpResponse.
public OutgoingWebResponse PrepareResponse(IProtocolMessage message) {
Requires.NotNull(message, "message");
Contract.Ensures(Contract.Result() != null);
this.ProcessOutgoingMessage(message);
Logger.Channel.DebugFormat("Sending message: {0}", message.GetType().Name);
OutgoingWebResponse 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[HttpResponseHeader.CacheControl] = "no-cache, no-store, max-age=0, must-revalidate";
result.Headers[HttpResponseHeader.Pragma] = "no-cache";
return result;
}
///
/// Gets the protocol message embedded in the given HTTP request, if present.
///
/// The deserialized message, if one is found. Null otherwise.
///
/// Requires an HttpContext.Current context.
///
/// Thrown when is null.
public IDirectedProtocolMessage ReadFromRequest() {
return this.ReadFromRequest(this.GetRequestFromContext());
}
///
/// Gets the protocol message embedded in the given HTTP request, if present.
///
/// The expected type of the message to be received.
/// The deserialized message, if one is found. Null otherwise.
/// True if the expected message was recognized and deserialized. False otherwise.
///
/// Requires an HttpContext.Current context.
///
/// Thrown when is null.
/// Thrown when a request message of an unexpected type is received.
public bool TryReadFromRequest(out TRequest request)
where TRequest : class, IProtocolMessage {
return TryReadFromRequest(this.GetRequestFromContext(), out request);
}
///
/// 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 deserialized message, if one is found. Null otherwise.
/// 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 bool TryReadFromRequest(HttpRequestInfo httpRequest, out TRequest request)
where TRequest : class, IProtocolMessage {
Requires.NotNull(httpRequest, "httpRequest");
Contract.Ensures(Contract.Result() == (Contract.ValueAtReturn(out request) != null));
IProtocolMessage untypedRequest = this.ReadFromRequest(httpRequest);
if (untypedRequest == null) {
request = null;
return false;
}
request = untypedRequest as TRequest;
ErrorUtilities.VerifyProtocol(request != null, MessagingStrings.UnexpectedMessageReceived, typeof(TRequest), untypedRequest.GetType());
return true;
}
///
/// Gets the protocol message embedded in the current HTTP request.
///
/// The expected type of the message to be received.
/// The deserialized message. Never null.
///
/// Requires an HttpContext.Current context.
///
/// Thrown when is 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 TRequest ReadFromRequest()
where TRequest : class, IProtocolMessage {
return this.ReadFromRequest(this.GetRequestFromContext());
}
///
/// 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 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 TRequest ReadFromRequest(HttpRequestInfo httpRequest)
where TRequest : class, IProtocolMessage {
Requires.NotNull(httpRequest, "httpRequest");
TRequest request;
if (this.TryReadFromRequest(httpRequest, out request)) {
return request;
} else {
throw ErrorUtilities.ThrowProtocol(MessagingStrings.ExpectedMessageNotReceived, typeof(TRequest));
}
}
///
/// Gets the protocol message that may be embedded in the given HTTP request.
///
/// The request to search for an embedded message.
/// The deserialized message, if one is found. Null otherwise.
public IDirectedProtocolMessage ReadFromRequest(HttpRequestInfo httpRequest) {
Requires.NotNull(httpRequest, "httpRequest");
if (Logger.Channel.IsInfoEnabled && httpRequest.UrlBeforeRewriting != null) {
Logger.Channel.InfoFormat("Scanning incoming request for messages: {0}", httpRequest.UrlBeforeRewriting.AbsoluteUri);
}
IDirectedProtocolMessage requestMessage = this.ReadFromRequestCore(httpRequest);
if (requestMessage != null) {
Logger.Channel.DebugFormat("Incoming request received: {0}", requestMessage.GetType().Name);
this.ProcessIncomingMessage(requestMessage);
}
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 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 TResponse Request(IDirectedProtocolMessage requestMessage)
where TResponse : class, IProtocolMessage {
Requires.NotNull(requestMessage, "requestMessage");
Contract.Ensures(Contract.Result() != null);
IProtocolMessage response = this.Request(requestMessage);
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 remote party's response. Guaranteed to never be null.
/// Thrown if the response does not include a protocol message.
public IProtocolMessage Request(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
this.ProcessOutgoingMessage(requestMessage);
Logger.Channel.DebugFormat("Sending {0} request.", requestMessage.GetType().Name);
var responseMessage = this.RequestCore(requestMessage);
ErrorUtilities.VerifyProtocol(responseMessage != null, MessagingStrings.ExpectedMessageNotReceived, typeof(IProtocolMessage).Name);
Logger.Channel.DebugFormat("Received {0} response.", responseMessage.GetType().Name);
this.ProcessIncomingMessage(responseMessage);
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.
///
/// Thrown when the message is somehow invalid.
/// This can be due to tampering, replay attack or expiration, among other things.
///
internal void ProcessIncomingMessageTestHook(IProtocolMessage message) {
this.ProcessIncomingMessage(message);
}
///
/// 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 HttpWebRequest 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 OutgoingWebResponse 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 deserialized message parts, if found. Null otherwise.
/// Thrown when the response is not valid.
internal IDictionary ReadFromResponseCoreTestHook(IncomingWebResponse response) {
return this.ReadFromResponseCore(response);
}
///
/// This method should NOT be called by derived types
/// except when sending ONE WAY request messages.
///
///
/// Prepares a message for transmit by applying signatures, nonces, etc.
///
/// The message to prepare for sending.
internal void ProcessOutgoingMessageTestHook(IProtocolMessage message) {
this.ProcessOutgoingMessage(message);
}
///
/// 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 HttpRequestInfo GetRequestFromContext() {
Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
Contract.Ensures(Contract.Result() != null);
Contract.Ensures(Contract.Result().Url != null);
Contract.Ensures(Contract.Result().RawUrl != null);
Contract.Ensures(Contract.Result().UrlBeforeRewriting != null);
Contract.Assume(HttpContext.Current.Request.Url != null);
Contract.Assume(HttpContext.Current.Request.RawUrl != null);
return new HttpRequestInfo(HttpContext.Current.Request);
}
///
/// 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(string httpMethod) {
if (string.Equals(httpMethod, "GET", StringComparison.Ordinal) ||
string.Equals(httpMethod, "HEAD", StringComparison.Ordinal) ||
string.Equals(httpMethod, "DELETE", StringComparison.Ordinal)) {
return false;
} else if (string.Equals(httpMethod, "POST", StringComparison.Ordinal) ||
string.Equals(httpMethod, "PUT", StringComparison.Ordinal)) {
return true;
} else {
throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod);
}
}
///
/// 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));
}
}
///
/// Gets the direct response of a direct HTTP request.
///
/// The web request.
/// The response to the web request.
/// Thrown on network or protocol errors.
protected virtual IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) {
Requires.NotNull(webRequest, "webRequest");
return this.WebRequestHandler.GetResponse(webRequest);
}
///
/// Submits a direct request message to some remote party and blocks waiting for an immediately reply.
///
/// The request message.
/// 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 IProtocolMessage RequestCore(IDirectedProtocolMessage request) {
Requires.NotNull(request, "request");
Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
HttpWebRequest webRequest = this.CreateHttpRequest(request);
IDictionary responseFields;
IDirectResponseProtocolMessage responseMessage;
using (IncomingWebResponse response = this.GetDirectResponse(webRequest)) {
if (response.ResponseStream == null) {
return null;
}
responseFields = this.ReadFromResponseCore(response);
if (responseFields == null) {
return null;
}
responseMessage = this.MessageFactory.GetNewResponseMessage(request, responseFields);
if (responseMessage == null) {
return null;
}
this.OnReceivingDirectResponse(response, responseMessage);
}
var messageAccessor = this.MessageDescriptions.GetAccessor(responseMessage);
messageAccessor.Deserialize(responseFields);
return responseMessage;
}
///
/// 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(IncomingWebResponse 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 deserialized message, if one is found. Null otherwise.
protected virtual IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) {
Requires.NotNull(request, "request");
Logger.Channel.DebugFormat("Incoming HTTP request: {0} {1}", request.HttpMethod, request.UrlBeforeRewriting.AbsoluteUri);
// Search Form data first, and if nothing is there search the QueryString
Contract.Assume(request.Form != null && request.QueryStringBeforeRewriting != null);
var fields = request.Form.ToDictionary();
if (fields.Count == 0 && request.HttpMethod != "POST") { // OpenID 2.0 section 4.1.2
fields = request.QueryStringBeforeRewriting.ToDictionary();
}
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 OutgoingWebResponse PrepareIndirectResponse(IDirectedProtocolMessage message) {
Requires.NotNull(message, "message");
Requires.True(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.True((message.HttpMethods & (HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest)) != 0, "message");
Contract.Ensures(Contract.Result() != null);
Contract.Assert(message != null && message.Recipient != null);
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
Contract.Assert(message != null && message.Recipient != null);
var fields = messageAccessor.Serialize();
OutgoingWebResponse 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[HttpResponseHeader.Location].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,
"Message too large for a HTTP GET, and HTTP POST is not allowed for this message type.");
}
// 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 OutgoingWebResponse Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary fields, bool payloadInFragment = false) {
Requires.NotNull(message, "message");
Requires.True(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.NotNull(fields, "fields");
Contract.Ensures(Contract.Result() != null);
// As part of this redirect, we include an HTML body in order to get passed some proxy filters
// such as WebSense.
WebHeaderCollection headers = new WebHeaderCollection();
UriBuilder builder = new UriBuilder(message.Recipient);
if (payloadInFragment) {
builder.AppendFragmentArgs(fields);
} else {
builder.AppendQueryArgs(fields);
}
headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri);
headers.Add(HttpResponseHeader.ContentType, "text/html; charset=utf-8");
Logger.Http.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri);
OutgoingWebResponse response = new OutgoingWebResponse {
Status = HttpStatusCode.Redirect,
Headers = headers,
Body = string.Format(CultureInfo.InvariantCulture, RedirectResponseBodyFormat, builder.Uri.AbsoluteUri),
OriginalMessage = message
};
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.
protected virtual OutgoingWebResponse CreateFormPostResponse(IDirectedProtocolMessage message, IDictionary fields) {
Requires.NotNull(message, "message");
Requires.True(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
Requires.NotNull(fields, "fields");
Contract.Ensures(Contract.Result() != null);
WebHeaderCollection headers = new WebHeaderCollection();
headers.Add(HttpResponseHeader.ContentType, "text/html");
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();
OutgoingWebResponse response = new OutgoingWebResponse {
Status = HttpStatusCode.OK,
Headers = headers,
Body = bodyWriter.ToString(),
OriginalMessage = message
};
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 deserialized message parts, if found. Null otherwise.
/// Thrown when the response is not valid.
protected abstract IDictionary ReadFromResponseCore(IncomingWebResponse response);
///
/// 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 HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) {
Requires.NotNull(request, "request");
Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
Contract.Ensures(Contract.Result() != null);
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 OutgoingWebResponse 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.")]
protected virtual string SerializeAsJson(IMessage message) {
Requires.NotNull(message, "message");
MessageDictionary messageDictionary = this.MessageDescriptions.GetAccessor(message);
using (var memoryStream = new MemoryStream()) {
using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream, Encoding.UTF8)) {
MessageSerializer.Serialize(messageDictionary, jsonWriter);
jsonWriter.Flush();
}
string json = Encoding.UTF8.GetString(memoryStream.ToArray());
return json;
}
}
///
/// Deserializes from flat data from a JSON object.
///
/// A JSON string.
/// The simple "key":"value" pairs from a JSON-encoded object.
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.
///
/// This method should NOT be called by derived types
/// except when sending ONE WAY request messages.
///
protected void ProcessOutgoingMessage(IProtocolMessage message) {
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) {
Contract.Assume(bindingElement.Channel != null);
MessageProtections? elementProtection = bindingElement.ProcessOutgoingMessage(message);
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 (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 HttpWebRequest InitializeRequestAsGet(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Requires.True(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);
HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri);
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 HttpWebRequest InitializeRequestAsHead(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Requires.True(requestMessage.Recipient != null, "requestMessage", MessagingStrings.DirectedMessageMissingRecipient);
HttpWebRequest request = this.InitializeRequestAsGet(requestMessage);
request.Method = "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 HttpWebRequest InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Contract.Ensures(Contract.Result() != null);
var messageAccessor = this.MessageDescriptions.GetAccessor(requestMessage);
var fields = messageAccessor.Serialize();
var httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient);
httpRequest.CachePolicy = this.CachePolicy;
httpRequest.Method = "POST";
var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData;
if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) {
var multiPartFields = new List(requestMessageWithBinaryData.BinaryData);
// When sending multi-part, all data gets send as multi-part -- even the non-binary data.
multiPartFields.AddRange(fields.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value)));
this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields);
} else {
ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart);
this.SendParametersInEntity(httpRequest, 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 HttpWebRequest InitializeRequestAsPut(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Contract.Ensures(Contract.Result() != null);
HttpWebRequest request = this.InitializeRequestAsGet(requestMessage);
request.Method = "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 HttpWebRequest InitializeRequestAsDelete(IDirectedProtocolMessage requestMessage) {
Requires.NotNull(requestMessage, "requestMessage");
Contract.Ensures(Contract.Result() != null);
HttpWebRequest request = this.InitializeRequestAsGet(requestMessage);
request.Method = "DELETE";
return request;
}
///
/// Sends the given parameters in the entity stream of an HTTP request.
///
/// The HTTP request.
/// The parameters to send.
///
/// This method calls and closes
/// the request stream, but does not call .
///
protected void SendParametersInEntity(HttpWebRequest httpRequest, IDictionary fields) {
Requires.NotNull(httpRequest, "httpRequest");
Requires.NotNull(fields, "fields");
string requestBody = MessagingUtilities.CreateQueryString(fields);
byte[] requestBytes = PostEntityEncoding.GetBytes(requestBody);
httpRequest.ContentType = HttpFormUrlEncodedContentType.ToString();
httpRequest.ContentLength = requestBytes.Length;
Stream requestStream = this.WebRequestHandler.GetRequestStream(httpRequest);
try {
requestStream.Write(requestBytes, 0, requestBytes.Length);
} finally {
// We need to be sure to close the request stream...
// unless it is a MemoryStream, which is a clue that we're in
// a mock stream situation and closing it would preclude reading it later.
if (!(requestStream is MemoryStream)) {
requestStream.Dispose();
}
}
}
///
/// Sends the given parameters in the entity stream of an HTTP request in multi-part format.
///
/// The HTTP request.
/// The parameters to send.
///
/// This method calls and closes
/// the request stream, but does not call .
///
protected void SendParametersInEntityAsMultipart(HttpWebRequest httpRequest, IEnumerable fields) {
httpRequest.PostMultipartNoGetResponse(this.WebRequestHandler, fields);
}
///
/// Verifies the integrity and applicability of an incoming message.
///
/// The message just received.
///
/// Thrown when the message is somehow invalid.
/// This can be due to tampering, replay attack or expiration, among other things.
///
protected virtual void ProcessIncomingMessage(IProtocolMessage message) {
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());
}
MessageProtections appliedProtection = MessageProtections.None;
foreach (IChannelBindingElement bindingElement in this.IncomingBindingElements) {
Contract.Assume(bindingElement.Channel != null); // CC bug: this.IncomingBindingElements ensures this... why must we assume it here?
MessageProtections? elementProtection = bindingElement.ProcessIncomingMessage(message);
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) {
}
///
/// 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.NullOrWithNoNullElements(elements, "elements");
Contract.Ensures(Contract.Result>() != null);
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;
}
///
/// 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
}
#if CONTRACTS_FULL
///
/// Verifies conditions that should be true for any valid state of this object.
///
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
[ContractInvariantMethod]
private void ObjectInvariant() {
Contract.Invariant(this.MessageDescriptions != null);
}
#endif
///
/// 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;
}
}
}