//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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.Text;
using System.Threading;
using System.Web;
using DotNetOpenAuth.Messaging.Reflection;
///
/// Manages sending direct messages to a remote party and receiving responses.
///
[ContractVerification(true)]
[ContractClass(typeof(ChannelContract))]
public abstract class Channel : IDisposable {
///
/// The encoding to use when writing out POST entity strings.
///
private static readonly Encoding PostEntityEncoding = new UTF8Encoding(false);
///
/// 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.
///
private const int IndirectMessageGetToPostThreshold = 2 * 1024; // 2KB, recommended by OpenID group
///
/// 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);
///
/// 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) {
Contract.Requires(messageTypeProvider != null);
ErrorUtilities.VerifyArgumentNotNull(messageTypeProvider, "messageTypeProvider");
this.messageTypeProvider = messageTypeProvider;
this.WebRequestHandler = new StandardWebRequestHandler();
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 message descriptions.
///
internal MessageDescriptionCollection MessageDescriptions {
get {
Contract.Ensures(Contract.Result() != null);
return this.messageDescriptions;
}
set {
Contract.Requires(value != null);
ErrorUtilities.VerifyArgumentNotNull(value, "value");
this.messageDescriptions = value;
}
}
///
/// 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 { 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 a tool that can figure out what kind of message is being received
/// so it can be deserialized.
///
protected IMessageFactory MessageFactory {
get { return this.messageTypeProvider; }
}
///
/// Gets or sets the cache policy to use for direct message requests.
///
/// Default is .
protected RequestCachePolicy CachePolicy {
get {
return this.cachePolicy;
}
set {
Contract.Requires(value != null);
ErrorUtilities.VerifyArgumentNotNull(value, "value");
this.cachePolicy = value;
}
}
///
/// 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.
///
public void Send(IProtocolMessage message) {
Contract.Requires(HttpContext.Current != null);
Contract.Requires(message != null);
this.PrepareResponse(message).Send();
}
///
/// 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) {
Contract.Requires(message != null);
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyArgumentNotNull(message, "message");
this.ProcessOutgoingMessage(message);
Logger.Channel.DebugFormat("Sending message: {0}", message.GetType().Name);
switch (message.Transport) {
case MessageTransport.Direct:
// This is a response to a direct message.
return this.PrepareDirectResponse(message);
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);
return this.PrepareIndirectResponse(directedMessage);
default:
throw ErrorUtilities.ThrowArgumentNamed(
"message",
MessagingStrings.UnrecognizedEnumValue,
"Transport",
message.Transport);
}
}
///
/// 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 {
Contract.Requires(httpRequest != null);
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 given HTTP request, if present.
///
/// The expected type of the message to be received.
/// The deserialized message.
///
/// 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 that may be 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, if one is found. Null otherwise.
/// 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 {
Contract.Requires(httpRequest != null);
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) {
Contract.Requires(httpRequest != null);
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 {
Contract.Requires(requestMessage != 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) {
Contract.Requires(requestMessage != null);
ErrorUtilities.VerifyArgumentNotNull(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
///
/// 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() {
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyHttpContext();
return new HttpRequestInfo(HttpContext.Current.Request);
}
///
/// 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) {
Contract.Requires(message != null);
ErrorUtilities.VerifyArgumentNotNull(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) {
Contract.Requires(webRequest != null);
ErrorUtilities.VerifyArgumentNotNull(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) {
Contract.Requires(request != null);
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);
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) {
Contract.Requires(request != null);
ErrorUtilities.VerifyArgumentNotNull(request, "request");
Logger.Channel.DebugFormat("Incoming HTTP request: {0}", request.Url.AbsoluteUri);
// Search Form data first, and if nothing is there search the QueryString
Contract.Assume(request.Form != null && request.QueryString != null);
var fields = request.Form.ToDictionary();
if (fields.Count == 0 && request.HttpMethod != "POST") { // OpenID 2.0 section 4.1.2
fields = request.QueryString.ToDictionary();
}
return (IDirectedProtocolMessage)this.Receive(fields, request.GetRecipient());
}
///
/// 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) {
Contract.Requires(fields != null);
ErrorUtilities.VerifyArgumentNotNull(fields, "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) {
Contract.Requires(message != null && message.Recipient != null);
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyArgumentNotNull(message, "message");
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
var fields = messageAccessor.Serialize();
// First try creating a 301 redirect, and fallback to a form POST
// if the message is too big.
OutgoingWebResponse response = this.Create301RedirectResponse(message, fields);
if (response.Headers[HttpResponseHeader.Location].Length > IndirectMessageGetToPostThreshold) {
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.
/// The encoded HTTP response.
protected virtual OutgoingWebResponse Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary fields) {
Contract.Requires(message != null && message.Recipient != null);
Contract.Requires(fields != null);
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyArgumentNotNull(message, "message");
ErrorUtilities.VerifyArgumentNamed(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
ErrorUtilities.VerifyArgumentNotNull(fields, "fields");
WebHeaderCollection headers = new WebHeaderCollection();
UriBuilder builder = new UriBuilder(message.Recipient);
MessagingUtilities.AppendQueryArgs(builder, fields);
headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri);
Logger.Http.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri);
OutgoingWebResponse response = new OutgoingWebResponse {
Status = HttpStatusCode.Redirect,
Headers = headers,
Body = null,
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) {
Contract.Requires(message != null && message.Recipient != null);
Contract.Requires(fields != null);
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyArgumentNotNull(message, "message");
ErrorUtilities.VerifyArgumentNamed(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
ErrorUtilities.VerifyArgumentNotNull(fields, "fields");
WebHeaderCollection headers = new WebHeaderCollection();
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) {
Contract.Requires(request != null);
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);
///
/// 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) {
Contract.Requires(message != null);
ErrorUtilities.VerifyArgumentNotNull(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) {
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 messageAccessor = this.MessageDescriptions.GetAccessor(message);
Logger.Channel.InfoFormat(
"Prepared outgoing {0} ({1}) message: {2}{3}",
message.GetType().Name,
message.Version,
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) {
Contract.Requires(requestMessage != null);
ErrorUtilities.VerifyArgumentNotNull(requestMessage, "requestMessage");
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 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) {
Contract.Requires(requestMessage != null);
Contract.Ensures(Contract.Result() != null);
ErrorUtilities.VerifyArgumentNotNull(requestMessage, "requestMessage");
var messageAccessor = this.MessageDescriptions.GetAccessor(requestMessage);
var fields = messageAccessor.Serialize();
HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient);
httpRequest.CachePolicy = this.CachePolicy;
httpRequest.Method = "POST";
httpRequest.ContentType = "application/x-www-form-urlencoded";
// Setting the content-encoding to "utf-8" causes Google to reply
// with a 415 UnsupportedMediaType. But adding it doesn't buy us
// anything specific, so it's disable it until we know how to get it right.
////httpRequest.Headers[HttpRequestHeader.ContentEncoding] = PostEntityEncoding.WebName;
string requestBody = MessagingUtilities.CreateQueryString(fields);
byte[] requestBytes = PostEntityEncoding.GetBytes(requestBody);
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();
}
}
return httpRequest;
}
///
/// 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) {
Contract.Requires(message != null);
if (Logger.Channel.IsInfoEnabled) {
var messageAccessor = this.MessageDescriptions.GetAccessor(message);
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) {
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();
}
///
/// 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) {
Contract.Requires(outgoingOrder != null);
Contract.Requires(incomingOrder != null);
ErrorUtilities.VerifyArgumentNotNull(outgoingOrder, "outgoingOrder");
ErrorUtilities.VerifyArgumentNotNull(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) {
Contract.Requires(elements == null || Contract.ForAll(elements, e => e != null));
Contract.Ensures(Contract.Result>() != null);
if (elements == null) {
return new IChannelBindingElement[0];
}
ErrorUtilities.VerifyArgumentNamed(!elements.Contains(null), "elements", MessagingStrings.SequenceContainsNullElement);
// 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
}
///
/// 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) {
Contract.Requires(message != null);
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) {
Contract.Requires(order != null);
ErrorUtilities.VerifyArgumentNotNull(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;
}
}
}