//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.ChannelElements {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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;
using Validation;
///
/// A channel that knows how to send and receive OpenID messages.
///
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) {
Requires.NotNull(messageTypeProvider, "messageTypeProvider");
// 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();
ApplyMessageTemplate(response, preparedResponse);
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;
}
}
}