//----------------------------------------------------------------------- // // 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 = @"
{1}
"; /// /// 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; } } }