//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.ChannelElements { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.Extensions; using DotNetOpenAuth.OpenId.Messages; /// /// A channel that knows how to send and receive OpenID messages. /// [ContractVerification(true)] internal class OpenIdChannel : Channel { /// /// The HTTP Content-Type to use in Key-Value Form responses. /// /// /// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain. But this value /// does not prevent free hosters like GoDaddy from tacking on their ads /// to the end of the direct response, corrupting the data. So we deviate /// from the spec a bit here to improve the story for free Providers. /// internal const string KeyValueFormContentType = "application/x-openid-kvf"; /// /// The encoder that understands how to read and write Key-Value Form. /// private KeyValueFormEncoding keyValueForm = new KeyValueFormEncoding(); /// /// 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 OpenIdChannel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements) : base(messageTypeProvider, bindingElements) { Contract.Requires(messageTypeProvider != null); // Customize the binding element order, since we play some tricks for higher // security and backward compatibility with older OpenID versions. var outgoingBindingElements = new List(bindingElements); var incomingBindingElements = new List(bindingElements); incomingBindingElements.Reverse(); // Customize the order of the incoming elements by moving the return_to elements in front. var backwardCompatibility = incomingBindingElements.OfType().SingleOrDefault(); var returnToSign = incomingBindingElements.OfType().SingleOrDefault(); if (backwardCompatibility != null) { incomingBindingElements.MoveTo(0, backwardCompatibility); } if (returnToSign != null) { // Yes, this is intentionally, shifting the backward compatibility // binding element to second position. incomingBindingElements.MoveTo(0, returnToSign); } this.CustomizeBindingElementOrder(outgoingBindingElements, incomingBindingElements); // Change out the standard web request handler to reflect the standard // OpenID pattern that outgoing web requests are to unknown and untrusted // servers on the Internet. this.WebRequestHandler = new UntrustedWebRequestHandler(); } /// /// Verifies the integrity and applicability of an incoming message. /// /// The message just received. /// /// Thrown when the message is somehow invalid, except for check_authentication messages. /// This can be due to tampering, replay attack or expiration, among other things. /// protected override void ProcessIncomingMessage(IProtocolMessage message) { var checkAuthRequest = message as CheckAuthenticationRequest; if (checkAuthRequest != null) { IndirectSignedResponse originalResponse = new IndirectSignedResponse(checkAuthRequest, this); try { base.ProcessIncomingMessage(originalResponse); checkAuthRequest.IsValid = true; } catch (ProtocolException) { checkAuthRequest.IsValid = false; } } else { base.ProcessIncomingMessage(message); } // Convert an OpenID indirect error message, which we never expect // between two good OpenID implementations, into an exception. // We don't process DirectErrorResponse because associate negotiations // commonly get a derivative of that message type and handle it. var errorMessage = message as IndirectErrorResponse; if (errorMessage != null) { string exceptionMessage = string.Format( CultureInfo.CurrentCulture, OpenIdStrings.IndirectErrorFormattedMessage, errorMessage.ErrorMessage, errorMessage.Contact, errorMessage.Reference); throw new ProtocolException(exceptionMessage, message); } } /// /// Prepares an HTTP request that carries a given message. /// /// The message to send. /// /// The prepared to send the request. /// protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { return this.InitializeRequestAsPost(request); } /// /// 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 override IDictionary ReadFromResponseCore(IncomingWebResponse response) { try { return this.keyValueForm.GetDictionary(response.ResponseStream); } catch (FormatException ex) { throw ErrorUtilities.Wrap(ex, ex.Message); } } /// /// Called when receiving a direct response message, before deserialization begins. /// /// The HTTP direct response. /// The newly instantiated message, prior to deserialization. protected override void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) { base.OnReceivingDirectResponse(response, message); // Verify that the expected HTTP status code was used for the message, // per OpenID 2.0 section 5.1.2.2. // Note: The v1.1 spec doesn't require 400 responses for some error messages if (message.Version.Major >= 2) { var httpDirectResponse = message as IHttpDirectResponse; if (httpDirectResponse != null) { ErrorUtilities.VerifyProtocol( httpDirectResponse.HttpStatusCode == response.Status, MessagingStrings.UnexpectedHttpStatusCode, (int)httpDirectResponse.HttpStatusCode, (int)response.Status); } } } /// /// 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 V1.0 section 5.3. /// protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { var messageAccessor = this.MessageDescriptions.GetAccessor(response); var fields = messageAccessor.Serialize(); byte[] keyValueEncoding = KeyValueFormEncoding.GetBytes(fields); OutgoingWebResponse preparedResponse = new OutgoingWebResponse(); preparedResponse.Headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType); preparedResponse.OriginalMessage = response; preparedResponse.ResponseStream = new MemoryStream(keyValueEncoding); IHttpDirectResponse httpMessage = response as IHttpDirectResponse; if (httpMessage != null) { preparedResponse.Status = httpMessage.HttpStatusCode; } return preparedResponse; } /// /// 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 override IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) { IncomingWebResponse response = this.WebRequestHandler.GetResponse(webRequest, DirectWebRequestOptions.AcceptAllHttpResponses); // Filter the responses to the allowable set of HTTP status codes. if (response.Status != HttpStatusCode.OK && response.Status != HttpStatusCode.BadRequest) { if (Logger.Channel.IsErrorEnabled) { using (var reader = new StreamReader(response.ResponseStream)) { Logger.Channel.ErrorFormat( "Unexpected HTTP status code {0} {1} received in direct response:{2}{3}", (int)response.Status, response.Status, Environment.NewLine, reader.ReadToEnd()); } } // Call dispose before throwing since we're not including the response in the // exception we're throwing. response.Dispose(); ErrorUtilities.ThrowProtocol(OpenIdStrings.UnexpectedHttpStatusCode, (int)response.Status, response.Status); } return response; } } }