summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Core/Messaging
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging')
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/BinaryDataBagFormatter.cs80
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs163
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd76
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs93
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs68
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs40
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs118
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs29
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs39
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs35
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs34
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs156
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs136
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs34
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs107
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs148
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs184
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Channel.cs1406
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ChannelContract.cs54
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ChannelEventArgs.cs30
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/DataBag.cs147
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs354
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/DirectWebRequestOptions.cs38
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/EmptyDictionary.cs250
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/EmptyEnumerator.cs65
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/EmptyList.cs211
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/EnumerableCache.cs243
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs365
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Exceptions.cd33
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/HostErrorException.cs66
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs58
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs423
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs146
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs75
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IDirectResponseProtocolMessage.cs17
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs223
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs.orig222
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IDirectedProtocolMessage.cs30
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IExtensionMessage.cs16
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponse.cs28
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponseContract.cs43
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IHttpIndirectResponse.cs22
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IMessage.cs100
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IMessageFactory.cs93
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IMessageOriginalPayload.cs40
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs156
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IMessageWithEvents.cs25
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IProtocolMessage.cs27
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IProtocolMessageWithExtensions.cs116
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IStreamSerializingDataBag.cs55
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ITamperResistantProtocolMessage.cs22
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs191
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/IncomingWebResponseContract.cs54
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/InternalErrorException.cs56
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/KeyedCollectionDelegate.cs45
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs121
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessageProtections.cs46
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessageReceivingEndpoint.cs54
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs236
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessageTransport.cs22
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Messaging.cd62
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs675
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx324
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagingStrings.sr.resx294
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs1709
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs223
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs116
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs300
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs40
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs93
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartEncoder.cs78
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartNullEncoder.cs18
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartOriginalEncoder.cs22
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescription.cs283
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescriptionCollection.cs217
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDictionary.cs409
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs428
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs67
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/StandardMessageFactory.cs298
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs111
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs249
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs55
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/TimestampEncoder.cs61
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/UnprotectedMessageException.cs37
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs476
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs77
86 files changed, 14286 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/BinaryDataBagFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/BinaryDataBagFormatter.cs
new file mode 100644
index 0000000..0c20955
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/BinaryDataBagFormatter.cs
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------
+// <copyright file="BinaryDataBagFormatter.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// A compact binary <see cref="DataBag"/> serialization class.
+ /// </summary>
+ /// <typeparam name="T">The <see cref="DataBag"/>-derived type to serialize/deserialize.</typeparam>
+ internal class BinaryDataBagFormatter<T> : DataBagFormatterBase<T> where T : DataBag, IStreamSerializingDataBag, new() {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BinaryDataBagFormatter&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="signingKey">The crypto service provider with the asymmetric key to use for signing or verifying the token.</param>
+ /// <param name="encryptingKey">The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected internal BinaryDataBagFormatter(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(signingKey, encryptingKey, compressed, maximumAge, decodeOnceOnly) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BinaryDataBagFormatter&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
+ /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
+ /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The minimum age.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected internal BinaryDataBagFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) {
+ Requires.True((cryptoKeyStore != null && bucket != null) || (!signed && !encrypted), null);
+ }
+
+ /// <summary>
+ /// Serializes the <see cref="DataBag"/> instance to a buffer.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>The buffer containing the serialized data.</returns>
+ protected override byte[] SerializeCore(T message) {
+ using (var stream = new MemoryStream()) {
+ message.Serialize(stream);
+ return stream.ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Deserializes the <see cref="DataBag"/> instance from a buffer.
+ /// </summary>
+ /// <param name="message">The message instance to initialize with data from the buffer.</param>
+ /// <param name="data">The data buffer.</param>
+ protected override void DeserializeCore(T message, byte[] data) {
+ using (var stream = new MemoryStream(data)) {
+ message.Deserialize(stream);
+ }
+
+ // Perform basic validation on message that the MessageSerializer would have normally performed.
+ var messageDescription = MessageDescriptions.Get(message);
+ var dictionary = messageDescription.GetDictionary(message);
+ messageDescription.EnsureMessagePartsPassBasicValidation(dictionary);
+ IMessage m = message;
+ m.EnsureValidMessage();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs
new file mode 100644
index 0000000..2691202
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/AsymmetricCryptoKeyStoreWrapper.cs
@@ -0,0 +1,163 @@
+//-----------------------------------------------------------------------
+// <copyright file="AsymmetricCryptoKeyStoreWrapper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Provides RSA encryption of symmetric keys to protect them from a theft of
+ /// the persistent store.
+ /// </summary>
+ public class AsymmetricCryptoKeyStoreWrapper : ICryptoKeyStore {
+ /// <summary>
+ /// The persistent store for asymmetrically encrypted symmetric keys.
+ /// </summary>
+ private readonly ICryptoKeyStore dataStore;
+
+ /// <summary>
+ /// The memory cache of decrypted keys.
+ /// </summary>
+ private readonly MemoryCryptoKeyStore cache = new MemoryCryptoKeyStore();
+
+ /// <summary>
+ /// The asymmetric algorithm to use encrypting/decrypting the symmetric keys.
+ /// </summary>
+ private readonly RSACryptoServiceProvider asymmetricCrypto;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AsymmetricCryptoKeyStoreWrapper"/> class.
+ /// </summary>
+ /// <param name="dataStore">The data store.</param>
+ /// <param name="asymmetricCrypto">The asymmetric protection to apply to symmetric keys. Must include the private key.</param>
+ public AsymmetricCryptoKeyStoreWrapper(ICryptoKeyStore dataStore, RSACryptoServiceProvider asymmetricCrypto) {
+ Requires.NotNull(dataStore, "dataStore");
+ Requires.NotNull(asymmetricCrypto, "asymmetricCrypto");
+ Requires.True(!asymmetricCrypto.PublicOnly, "asymmetricCrypto");
+ this.dataStore = dataStore;
+ this.asymmetricCrypto = asymmetricCrypto;
+ }
+
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ var key = this.dataStore.GetKey(bucket, handle);
+ return this.Decrypt(bucket, handle, key);
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return this.dataStore.GetKeys(bucket)
+ .Select(pair => new KeyValuePair<string, CryptoKey>(pair.Key, this.Decrypt(bucket, pair.Key, pair.Value)));
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="decryptedCryptoKey">The key to store.</param>
+ [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "2#", Justification = "Helps readability because multiple keys are involved.")]
+ public void StoreKey(string bucket, string handle, CryptoKey decryptedCryptoKey) {
+ byte[] encryptedKey = this.asymmetricCrypto.Encrypt(decryptedCryptoKey.Key, true);
+ var encryptedCryptoKey = new CryptoKey(encryptedKey, decryptedCryptoKey.ExpiresUtc);
+ this.dataStore.StoreKey(bucket, handle, encryptedCryptoKey);
+
+ this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey));
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ this.dataStore.RemoveKey(bucket, handle);
+ this.cache.RemoveKey(bucket, handle);
+ }
+
+ /// <summary>
+ /// Decrypts the specified key.
+ /// </summary>
+ /// <param name="bucket">The bucket.</param>
+ /// <param name="handle">The handle.</param>
+ /// <param name="encryptedCryptoKey">The encrypted key.</param>
+ /// <returns>
+ /// The decrypted key.
+ /// </returns>
+ [SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "2#", Justification = "Helps readability because multiple keys are involved.")]
+ private CryptoKey Decrypt(string bucket, string handle, CryptoKey encryptedCryptoKey) {
+ if (encryptedCryptoKey == null) {
+ return null;
+ }
+
+ // Avoid the asymmetric decryption if possible by looking up whether we have that in our cache.
+ CachedCryptoKey cached = (CachedCryptoKey)this.cache.GetKey(bucket, handle);
+ if (cached != null && MessagingUtilities.AreEquivalent(cached.EncryptedKey, encryptedCryptoKey.Key)) {
+ return cached;
+ }
+
+ byte[] decryptedKey = this.asymmetricCrypto.Decrypt(encryptedCryptoKey.Key, true);
+ var decryptedCryptoKey = new CryptoKey(decryptedKey, encryptedCryptoKey.ExpiresUtc);
+
+ // Store the decrypted version in the cache to save time next time.
+ this.cache.StoreKey(bucket, handle, new CachedCryptoKey(encryptedCryptoKey, decryptedCryptoKey));
+
+ return decryptedCryptoKey;
+ }
+
+ /// <summary>
+ /// An encrypted key and its decrypted equivalent.
+ /// </summary>
+ private class CachedCryptoKey : CryptoKey {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachedCryptoKey"/> class.
+ /// </summary>
+ /// <param name="encrypted">The encrypted key.</param>
+ /// <param name="decrypted">The decrypted key.</param>
+ internal CachedCryptoKey(CryptoKey encrypted, CryptoKey decrypted)
+ : base(decrypted.Key, decrypted.ExpiresUtc) {
+ Contract.Requires(encrypted != null);
+ Contract.Requires(decrypted != null);
+ Contract.Requires(encrypted.ExpiresUtc == decrypted.ExpiresUtc);
+
+ this.EncryptedKey = encrypted.Key;
+ }
+
+ /// <summary>
+ /// Gets the encrypted key.
+ /// </summary>
+ internal byte[] EncryptedKey { get; private set; }
+
+ /// <summary>
+ /// Invariant conditions.
+ /// </summary>
+ [ContractInvariantMethod]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.EncryptedKey != null);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd b/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd
new file mode 100644
index 0000000..e52e81e
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/Bindings.cd
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.InvalidSignatureException" Collapsed="true">
+ <Position X="8.25" Y="2" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\InvalidSignatureException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.ReplayedMessageException" Collapsed="true">
+ <Position X="6" Y="2" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\ReplayedMessageException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.ExpiredMessageException" Collapsed="true">
+ <Position X="3.75" Y="2" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\ExpiredMessageException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.ProtocolException" Collapsed="true">
+ <Position X="6" Y="0.5" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>ICAMAAAAQAAAgAEAAIBAAAYgCgAAIAAAIACAACAAAAA=</HashCode>
+ <FileName>Messaging\ProtocolException.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.StandardExpirationBindingElement" Collapsed="true">
+ <Position X="1" Y="3" Width="2.75" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAgAAAARAAEAAAAAAAAAAIAAAAAAEAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\StandardExpirationBindingElement.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Interface Name="DotNetOpenAuth.Messaging.IProtocolMessage" Collapsed="true">
+ <Position X="6" Y="3.5" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\IProtocolMessage.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.Messaging.Bindings.IExpiringProtocolMessage" Collapsed="true">
+ <Position X="3.75" Y="4.75" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\IExpiringProtocolMessage.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.Messaging.Bindings.IReplayProtectedProtocolMessage" Collapsed="true">
+ <Position X="6" Y="4.75" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAA=</HashCode>
+ <FileName>Messaging\Bindings\IReplayProtectedProtocolMessage.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.Messaging.IChannelBindingElement">
+ <Position X="0.5" Y="0.5" Width="2" />
+ <TypeIdentifier>
+ <HashCode>BAAAAAAgAAAAAAAEAAAAAAAAAAAAAAAAAEAAAAAAAAA=</HashCode>
+ <FileName>Messaging\IChannelBindingElement.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.OAuth.ChannelElements.ITamperResistantOAuthMessage" Collapsed="true">
+ <Position X="8.75" Y="4.75" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AIAAAAAAAAAAgAAAAIAAAgAAAAAAIAQAAAAAAAAAAAA=</HashCode>
+ <FileName>OAuth\ChannelElements\ITamperResistantOAuthMessage.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs
new file mode 100644
index 0000000..7160014
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKey.cs
@@ -0,0 +1,93 @@
+//-----------------------------------------------------------------------
+// <copyright file="CryptoKey.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A cryptographic key and metadata concerning it.
+ /// </summary>
+ public class CryptoKey {
+ /// <summary>
+ /// Backing field for the <see cref="Key"/> property.
+ /// </summary>
+ private readonly byte[] key;
+
+ /// <summary>
+ /// Backing field for the <see cref="ExpiresUtc"/> property.
+ /// </summary>
+ private readonly DateTime expiresUtc;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKey"/> class.
+ /// </summary>
+ /// <param name="key">The cryptographic key.</param>
+ /// <param name="expiresUtc">The expires UTC.</param>
+ public CryptoKey(byte[] key, DateTime expiresUtc) {
+ Requires.NotNull(key, "key");
+ Requires.True(expiresUtc.Kind == DateTimeKind.Utc, "expiresUtc");
+ this.key = key;
+ this.expiresUtc = expiresUtc;
+ }
+
+ /// <summary>
+ /// Gets the key.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's a buffer")]
+ public byte[] Key {
+ get {
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+ return this.key;
+ }
+ }
+
+ /// <summary>
+ /// Gets the expiration date of this key (UTC time).
+ /// </summary>
+ public DateTime ExpiresUtc {
+ get {
+ Contract.Ensures(Contract.Result<DateTime>().Kind == DateTimeKind.Utc);
+ return this.expiresUtc;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as CryptoKey;
+ if (other == null) {
+ return false;
+ }
+
+ return this.ExpiresUtc == other.ExpiresUtc
+ && MessagingUtilities.AreEquivalent(this.Key, other.Key);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.ExpiresUtc.GetHashCode();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs
new file mode 100644
index 0000000..ebd29de
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/CryptoKeyCollisionException.cs
@@ -0,0 +1,68 @@
+//-----------------------------------------------------------------------
+// <copyright file="CryptoKeyCollisionException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Security.Permissions;
+
+ /// <summary>
+ /// Thrown by a hosting application or web site when a cryptographic key is created with a
+ /// bucket and handle that conflicts with a previously stored and unexpired key.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "Specialized exception has no need of a message parameter.")]
+ [Serializable]
+ public class CryptoKeyCollisionException : ArgumentException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ public CryptoKeyCollisionException() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ /// <param name="inner">The inner exception to include.</param>
+ public CryptoKeyCollisionException(Exception inner) : base(null, inner) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyCollisionException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected CryptoKeyCollisionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with information about the exception.
+ /// </summary>
+ /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic).
+ /// </exception>
+ /// <PermissionSet>
+ /// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/>
+ /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/>
+ /// </PermissionSet>
+#if CLR4
+ [System.Security.SecurityCritical]
+#else
+ [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
+#endif
+ public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) {
+ base.GetObjectData(info, context);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs
new file mode 100644
index 0000000..196946d
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExpiredMessageException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+
+ /// <summary>
+ /// An exception thrown when a message is received that exceeds the maximum message age limit.
+ /// </summary>
+ [Serializable]
+ internal class ExpiredMessageException : ProtocolException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExpiredMessageException"/> class.
+ /// </summary>
+ /// <param name="utcExpirationDate">The date the message expired.</param>
+ /// <param name="faultedMessage">The expired message.</param>
+ public ExpiredMessageException(DateTime utcExpirationDate, IProtocolMessage faultedMessage)
+ : base(string.Format(CultureInfo.CurrentCulture, MessagingStrings.ExpiredMessage, utcExpirationDate.ToLocalTime(), DateTime.Now), faultedMessage) {
+ Requires.True(utcExpirationDate.Kind == DateTimeKind.Utc, "utcExpirationDate");
+ Requires.NotNull(faultedMessage, "faultedMessage");
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExpiredMessageException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected ExpiredMessageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs
new file mode 100644
index 0000000..861ba89
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ICryptoKeyStore.cs
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------
+// <copyright file="ICryptoKeyStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A persistent store for rotating symmetric cryptographic keys.
+ /// </summary>
+ /// <remarks>
+ /// Implementations should persist it in such a way that the keys are shared across all servers
+ /// on a web farm, where applicable.
+ /// The store should consider protecting the persistent store against theft resulting in the loss
+ /// of the confidentiality of the keys. One possible mitigation is to asymmetrically encrypt
+ /// each key using a certificate installed in the server's certificate store.
+ /// </remarks>
+ [ContractClass(typeof(ICryptoKeyStoreContract))]
+ public interface ICryptoKeyStore {
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>The cryptographic key, or <c>null</c> if no matching key was found.</returns>
+ CryptoKey GetKey(string bucket, string handle);
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Important for scalability")]
+ IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket);
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ void StoreKey(string bucket, string handle, CryptoKey key);
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ void RemoveKey(string bucket, string handle);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="ICryptoKeyStore"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(ICryptoKeyStore))]
+ internal abstract class ICryptoKeyStoreContract : ICryptoKeyStore {
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ CryptoKey ICryptoKeyStore.GetKey(string bucket, string handle) {
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Requires.NotNullOrEmpty(handle, "handle");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ IEnumerable<KeyValuePair<string, CryptoKey>> ICryptoKeyStore.GetKeys(string bucket) {
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Contract.Ensures(Contract.Result<IEnumerable<KeyValuePair<string, CryptoKey>>>() != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ void ICryptoKeyStore.StoreKey(string bucket, string handle, CryptoKey key) {
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Requires.NotNullOrEmpty(handle, "handle");
+ Requires.NotNull(key, "key");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ void ICryptoKeyStore.RemoveKey(string bucket, string handle) {
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Requires.NotNullOrEmpty(handle, "handle");
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs
new file mode 100644
index 0000000..fc43ae6
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/IExpiringProtocolMessage.cs
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------
+// <copyright file="IExpiringProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+
+ /// <summary>
+ /// The contract a message that has an allowable time window for processing must implement.
+ /// </summary>
+ /// <remarks>
+ /// All expiring messages must also be signed to prevent tampering with the creation date.
+ /// </remarks>
+ internal interface IExpiringProtocolMessage : IProtocolMessage {
+ /// <summary>
+ /// Gets or sets the UTC date/time the message was originally sent onto the network.
+ /// </summary>
+ /// <remarks>
+ /// The property setter should ensure a UTC date/time,
+ /// and throw an exception if this is not possible.
+ /// </remarks>
+ /// <exception cref="ArgumentException">
+ /// Thrown when a DateTime that cannot be converted to UTC is set.
+ /// </exception>
+ DateTime UtcCreationDate { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs
new file mode 100644
index 0000000..7a3e8bb
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/INonceStore.cs
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------
+// <copyright file="INonceStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+
+ /// <summary>
+ /// Describes the contract a nonce store must fulfill.
+ /// </summary>
+ public interface INonceStore {
+ /// <summary>
+ /// Stores a given nonce and timestamp.
+ /// </summary>
+ /// <param name="context">The context, or namespace, within which the
+ /// <paramref name="nonce"/> must be unique.
+ /// The context SHOULD be treated as case-sensitive.
+ /// The value will never be <c>null</c> but may be the empty string.</param>
+ /// <param name="nonce">A series of random characters.</param>
+ /// <param name="timestampUtc">The UTC timestamp that together with the nonce string make it unique
+ /// within the given <paramref name="context"/>.
+ /// The timestamp may also be used by the data store to clear out old nonces.</param>
+ /// <returns>
+ /// True if the context+nonce+timestamp (combination) was not previously in the database.
+ /// False if the nonce was stored previously with the same timestamp and context.
+ /// </returns>
+ /// <remarks>
+ /// The nonce must be stored for no less than the maximum time window a message may
+ /// be processed within before being discarded as an expired message.
+ /// This maximum message age can be looked up via the
+ /// <see cref="DotNetOpenAuth.Configuration.MessagingElement.MaximumMessageLifetime"/>
+ /// property, accessible via the <see cref="DotNetOpenAuth.Configuration.MessagingElement.Configuration"/>
+ /// property.
+ /// </remarks>
+ bool StoreNonce(string context, string nonce, DateTime timestampUtc);
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs
new file mode 100644
index 0000000..1edf934
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/IReplayProtectedProtocolMessage.cs
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------
+// <copyright file="IReplayProtectedProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// The contract a message that has an allowable time window for processing must implement.
+ /// </summary>
+ /// <remarks>
+ /// All replay-protected messages must also be set to expire so the nonces do not have
+ /// to be stored indefinitely.
+ /// </remarks>
+ internal interface IReplayProtectedProtocolMessage : IExpiringProtocolMessage, IDirectedProtocolMessage {
+ /// <summary>
+ /// Gets the context within which the nonce must be unique.
+ /// </summary>
+ /// <value>
+ /// The value of this property must be a value assigned by the nonce consumer
+ /// to represent the entity that generated the nonce. The value must never be
+ /// <c>null</c> but may be the empty string.
+ /// This value is treated as case-sensitive.
+ /// </value>
+ string NonceContext { get; }
+
+ /// <summary>
+ /// Gets or sets the nonce that will protect the message from replay attacks.
+ /// </summary>
+ string Nonce { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs
new file mode 100644
index 0000000..28b7e96
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/InvalidSignatureException.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+// <copyright file="InvalidSignatureException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+
+ /// <summary>
+ /// An exception thrown when a signed message does not pass signature validation.
+ /// </summary>
+ [Serializable]
+ internal class InvalidSignatureException : ProtocolException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InvalidSignatureException"/> class.
+ /// </summary>
+ /// <param name="faultedMessage">The message with the invalid signature.</param>
+ public InvalidSignatureException(IProtocolMessage faultedMessage)
+ : base(MessagingStrings.SignatureInvalid, faultedMessage) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InvalidSignatureException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected InvalidSignatureException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs
new file mode 100644
index 0000000..63d1953
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/MemoryCryptoKeyStore.cs
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------
+// <copyright file="MemoryCryptoKeyStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ /// <summary>
+ /// A in-memory store of crypto keys.
+ /// </summary>
+ internal class MemoryCryptoKeyStore : ICryptoKeyStore {
+ /// <summary>
+ /// How frequently to check for and remove expired secrets.
+ /// </summary>
+ private static readonly TimeSpan cleaningInterval = TimeSpan.FromMinutes(30);
+
+ /// <summary>
+ /// An in-memory cache of decrypted symmetric keys.
+ /// </summary>
+ /// <remarks>
+ /// The key is the bucket name. The value is a dictionary whose key is the handle and whose value is the cached key.
+ /// </remarks>
+ private readonly Dictionary<string, Dictionary<string, CryptoKey>> store = new Dictionary<string, Dictionary<string, CryptoKey>>(StringComparer.Ordinal);
+
+ /// <summary>
+ /// The last time the cache had expired keys removed from it.
+ /// </summary>
+ private DateTime lastCleaning = DateTime.UtcNow;
+
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ CryptoKey key;
+ if (cacheBucket.TryGetValue(handle, out key)) {
+ return key;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ return cacheBucket.ToList();
+ } else {
+ return Enumerable.Empty<KeyValuePair<string, CryptoKey>>();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (!this.store.TryGetValue(bucket, out cacheBucket)) {
+ this.store[bucket] = cacheBucket = new Dictionary<string, CryptoKey>(StringComparer.Ordinal);
+ }
+
+ if (cacheBucket.ContainsKey(handle)) {
+ throw new CryptoKeyCollisionException();
+ }
+
+ cacheBucket[handle] = key;
+
+ this.CleanExpiredKeysFromMemoryCacheIfAppropriate();
+ }
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ lock (this.store) {
+ Dictionary<string, CryptoKey> cacheBucket;
+ if (this.store.TryGetValue(bucket, out cacheBucket)) {
+ cacheBucket.Remove(handle);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Cleans the expired keys from memory cache if the cleaning interval has passed.
+ /// </summary>
+ private void CleanExpiredKeysFromMemoryCacheIfAppropriate() {
+ if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
+ lock (this.store) {
+ if (DateTime.UtcNow > this.lastCleaning + cleaningInterval) {
+ this.ClearExpiredKeysFromMemoryCache();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Weeds out expired keys from the in-memory cache.
+ /// </summary>
+ private void ClearExpiredKeysFromMemoryCache() {
+ lock (this.store) {
+ var emptyBuckets = new List<string>();
+ foreach (var bucketPair in this.store) {
+ var expiredKeys = new List<string>();
+ foreach (var handlePair in bucketPair.Value) {
+ if (handlePair.Value.ExpiresUtc < DateTime.UtcNow) {
+ expiredKeys.Add(handlePair.Key);
+ }
+ }
+
+ foreach (var expiredKey in expiredKeys) {
+ bucketPair.Value.Remove(expiredKey);
+ }
+
+ if (bucketPair.Value.Count == 0) {
+ emptyBuckets.Add(bucketPair.Key);
+ }
+ }
+
+ foreach (string emptyBucket in emptyBuckets) {
+ this.store.Remove(emptyBucket);
+ }
+
+ this.lastCleaning = DateTime.UtcNow;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs
new file mode 100644
index 0000000..6e64acc
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/NonceMemoryStore.cs
@@ -0,0 +1,136 @@
+//-----------------------------------------------------------------------
+// <copyright file="NonceMemoryStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An in-memory nonce store. Useful for single-server web applications.
+ /// NOT for web farms.
+ /// </summary>
+ internal class NonceMemoryStore : INonceStore {
+ /// <summary>
+ /// How frequently we should take time to clear out old nonces.
+ /// </summary>
+ private const int AutoCleaningFrequency = 10;
+
+ /// <summary>
+ /// The maximum age a message can be before it is discarded.
+ /// </summary>
+ /// <remarks>
+ /// This is useful for knowing how long used nonces must be retained.
+ /// </remarks>
+ private readonly TimeSpan maximumMessageAge;
+
+ /// <summary>
+ /// A list of the consumed nonces.
+ /// </summary>
+ private readonly SortedDictionary<DateTime, List<string>> usedNonces = new SortedDictionary<DateTime, List<string>>();
+
+ /// <summary>
+ /// A lock object used around accesses to the <see cref="usedNonces"/> field.
+ /// </summary>
+ private object nonceLock = new object();
+
+ /// <summary>
+ /// Where we're currently at in our periodic nonce cleaning cycle.
+ /// </summary>
+ private int nonceClearingCounter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class.
+ /// </summary>
+ internal NonceMemoryStore()
+ : this(StandardExpirationBindingElement.MaximumMessageAge) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class.
+ /// </summary>
+ /// <param name="maximumMessageAge">The maximum age a message can be before it is discarded.</param>
+ internal NonceMemoryStore(TimeSpan maximumMessageAge) {
+ this.maximumMessageAge = maximumMessageAge;
+ }
+
+ #region INonceStore Members
+
+ /// <summary>
+ /// Stores a given nonce and timestamp.
+ /// </summary>
+ /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param>
+ /// <param name="nonce">A series of random characters.</param>
+ /// <param name="timestamp">The timestamp that together with the nonce string make it unique.
+ /// The timestamp may also be used by the data store to clear out old nonces.</param>
+ /// <returns>
+ /// True if the nonce+timestamp (combination) was not previously in the database.
+ /// False if the nonce was stored previously with the same timestamp.
+ /// </returns>
+ /// <remarks>
+ /// The nonce must be stored for no less than the maximum time window a message may
+ /// be processed within before being discarded as an expired message.
+ /// If the binding element is applicable to your channel, this expiration window
+ /// is retrieved or set using the
+ /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property.
+ /// </remarks>
+ public bool StoreNonce(string context, string nonce, DateTime timestamp) {
+ if (timestamp.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow) {
+ // The expiration binding element should have taken care of this, but perhaps
+ // it's at the boundary case. We should fail just to be safe.
+ return false;
+ }
+
+ // We just concatenate the context with the nonce to form a complete, namespace-protected nonce.
+ string completeNonce = context + "\0" + nonce;
+
+ lock (this.nonceLock) {
+ List<string> nonces;
+ if (!this.usedNonces.TryGetValue(timestamp, out nonces)) {
+ this.usedNonces[timestamp] = nonces = new List<string>(4);
+ }
+
+ if (nonces.Contains(completeNonce)) {
+ return false;
+ }
+
+ nonces.Add(completeNonce);
+
+ // Clear expired nonces if it's time to take a moment to do that.
+ // Unchecked so that this can int overflow without an exception.
+ unchecked {
+ this.nonceClearingCounter++;
+ }
+ if (this.nonceClearingCounter % AutoCleaningFrequency == 0) {
+ this.ClearExpiredNonces();
+ }
+
+ return true;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Clears consumed nonces from the cache that are so old they would be
+ /// rejected if replayed because it is expired.
+ /// </summary>
+ public void ClearExpiredNonces() {
+ lock (this.nonceLock) {
+ var oldNonceLists = this.usedNonces.Keys.Where(time => time.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow).ToList();
+ foreach (DateTime time in oldNonceLists) {
+ this.usedNonces.Remove(time);
+ }
+
+ // Reset the auto-clean counter so that if this method was called externally
+ // we don't auto-clean right away.
+ this.nonceClearingCounter = 0;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs
new file mode 100644
index 0000000..2b8df9d
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ReplayedMessageException.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+// <copyright file="ReplayedMessageException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+
+ /// <summary>
+ /// An exception thrown when a message is received for the second time, signalling a possible
+ /// replay attack.
+ /// </summary>
+ [Serializable]
+ internal class ReplayedMessageException : ProtocolException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReplayedMessageException"/> class.
+ /// </summary>
+ /// <param name="faultedMessage">The replayed message.</param>
+ public ReplayedMessageException(IProtocolMessage faultedMessage) : base(MessagingStrings.ReplayAttackDetected, faultedMessage) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReplayedMessageException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected ReplayedMessageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs
new file mode 100644
index 0000000..f8c8c6a
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardExpirationBindingElement.cs
@@ -0,0 +1,107 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardExpirationBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using DotNetOpenAuth.Configuration;
+
+ /// <summary>
+ /// A message expiration enforcing binding element that supports messages
+ /// implementing the <see cref="IExpiringProtocolMessage"/> interface.
+ /// </summary>
+ internal class StandardExpirationBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardExpirationBindingElement"/> class.
+ /// </summary>
+ internal StandardExpirationBindingElement() {
+ }
+
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets the protection offered by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.Expiration"/></value>
+ MessageProtections IChannelBindingElement.Protection {
+ get { return MessageProtections.Expiration; }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ public Channel Channel { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the maximum age a message implementing the
+ /// <see cref="IExpiringProtocolMessage"/> interface can be before
+ /// being discarded as too old.
+ /// </summary>
+ protected internal static TimeSpan MaximumMessageAge {
+ get { return Configuration.DotNetOpenAuthSection.Messaging.MaximumMessageLifetime; }
+ }
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Sets the timestamp on an outgoing message.
+ /// </summary>
+ /// <param name="message">The outgoing message.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ expiringMessage.UtcCreationDate = DateTime.UtcNow;
+ return MessageProtections.Expiration;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Reads the timestamp on a message and throws an exception if the message is too old.
+ /// </summary>
+ /// <param name="message">The incoming message.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ExpiredMessageException">Thrown if the given message has already expired.</exception>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ // Yes the UtcCreationDate is supposed to always be in UTC already,
+ // but just in case a given message failed to guarantee that, we do it here.
+ DateTime creationDate = expiringMessage.UtcCreationDate.ToUniversalTimeSafe();
+ DateTime expirationDate = creationDate + MaximumMessageAge;
+ if (expirationDate < DateTime.UtcNow) {
+ throw new ExpiredMessageException(expirationDate, expiringMessage);
+ }
+
+ // Mitigate HMAC attacks (just guessing the signature until they get it) by
+ // disallowing post-dated messages.
+ ErrorUtilities.VerifyProtocol(
+ creationDate <= DateTime.UtcNow + DotNetOpenAuthSection.Messaging.MaximumClockSkew,
+ MessagingStrings.MessageTimestampInFuture,
+ creationDate);
+
+ return MessageProtections.Expiration;
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs
new file mode 100644
index 0000000..78fd1d5
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/StandardReplayProtectionBindingElement.cs
@@ -0,0 +1,148 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardReplayProtectionBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A binding element that checks/verifies a nonce message part.
+ /// </summary>
+ internal class StandardReplayProtectionBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// These are the characters that may be chosen from when forming a random nonce.
+ /// </summary>
+ private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ /// <summary>
+ /// The persistent store for nonces received.
+ /// </summary>
+ private INonceStore nonceStore;
+
+ /// <summary>
+ /// The length of generated nonces.
+ /// </summary>
+ private int nonceLength = 8;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardReplayProtectionBindingElement"/> class.
+ /// </summary>
+ /// <param name="nonceStore">The store where nonces will be persisted and checked.</param>
+ internal StandardReplayProtectionBindingElement(INonceStore nonceStore)
+ : this(nonceStore, false) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardReplayProtectionBindingElement"/> class.
+ /// </summary>
+ /// <param name="nonceStore">The store where nonces will be persisted and checked.</param>
+ /// <param name="allowEmptyNonces">A value indicating whether zero-length nonces will be allowed.</param>
+ internal StandardReplayProtectionBindingElement(INonceStore nonceStore, bool allowEmptyNonces) {
+ Requires.NotNull(nonceStore, "nonceStore");
+
+ this.nonceStore = nonceStore;
+ this.AllowZeroLengthNonce = allowEmptyNonces;
+ }
+
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets the protection that this binding element provides messages.
+ /// </summary>
+ public MessageProtections Protection {
+ get { return MessageProtections.ReplayProtection; }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ public Channel Channel { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets the strength of the nonce, which is measured by the number of
+ /// nonces that could theoretically be generated.
+ /// </summary>
+ /// <remarks>
+ /// The strength of the nonce is equal to the number of characters that might appear
+ /// in the nonce to the power of the length of the nonce.
+ /// </remarks>
+ internal double NonceStrength {
+ get {
+ return Math.Pow(AllowedCharacters.Length, this.nonceLength);
+ }
+
+ set {
+ value = Math.Max(value, AllowedCharacters.Length);
+ this.nonceLength = (int)Math.Log(value, AllowedCharacters.Length);
+ Debug.Assert(this.nonceLength > 0, "Nonce length calculated to be below 1!");
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether empty nonces are allowed.
+ /// </summary>
+ /// <value>Default is <c>false</c>.</value>
+ internal bool AllowZeroLengthNonce { get; set; }
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Applies a nonce to the message.
+ /// </summary>
+ /// <param name="message">The message to apply replay protection to.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
+ if (nonceMessage != null) {
+ nonceMessage.Nonce = this.GenerateUniqueFragment();
+ return MessageProtections.ReplayProtection;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Verifies that the nonce in an incoming message has not been seen before.
+ /// </summary>
+ /// <param name="message">The incoming message.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ReplayedMessageException">Thrown when the nonce check revealed a replayed message.</exception>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
+ if (nonceMessage != null && nonceMessage.Nonce != null) {
+ ErrorUtilities.VerifyProtocol(nonceMessage.Nonce.Length > 0 || this.AllowZeroLengthNonce, MessagingStrings.InvalidNonceReceived);
+
+ if (!this.nonceStore.StoreNonce(nonceMessage.NonceContext, nonceMessage.Nonce, nonceMessage.UtcCreationDate)) {
+ Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", nonceMessage.Nonce, nonceMessage.UtcCreationDate);
+ throw new ReplayedMessageException(message);
+ }
+
+ return MessageProtections.ReplayProtection;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Generates a string of random characters for use as a nonce.
+ /// </summary>
+ /// <returns>The nonce string.</returns>
+ private string GenerateUniqueFragment() {
+ return MessagingUtilities.GetRandomString(this.nonceLength, AllowedCharacters);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs
new file mode 100644
index 0000000..2f3a1d9
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/CachedDirectWebResponse.cs
@@ -0,0 +1,184 @@
+//-----------------------------------------------------------------------
+// <copyright file="CachedDirectWebResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using System.Text;
+
+ /// <summary>
+ /// Cached details on the response from a direct web request to a remote party.
+ /// </summary>
+ [ContractVerification(true)]
+ [DebuggerDisplay("{Status} {ContentType.MediaType}, length: {ResponseStream.Length}")]
+ internal class CachedDirectWebResponse : IncomingWebResponse {
+ /// <summary>
+ /// A seekable, repeatable response stream.
+ /// </summary>
+ private MemoryStream responseStream;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class.
+ /// </summary>
+ internal CachedDirectWebResponse() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class.
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ /// <param name="response">The response.</param>
+ /// <param name="maximumBytesToRead">The maximum bytes to read.</param>
+ internal CachedDirectWebResponse(Uri requestUri, HttpWebResponse response, int maximumBytesToRead)
+ : base(requestUri, response) {
+ Requires.NotNull(requestUri, "requestUri");
+ Requires.NotNull(response, "response");
+ this.responseStream = CacheNetworkStreamAndClose(response, maximumBytesToRead);
+
+ // BUGBUG: if the response was exactly maximumBytesToRead, we'll incorrectly believe it was truncated.
+ this.ResponseTruncated = this.responseStream.Length == maximumBytesToRead;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CachedDirectWebResponse"/> class.
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ /// <param name="responseUri">The final URI to respond to the request.</param>
+ /// <param name="headers">The headers.</param>
+ /// <param name="statusCode">The status code.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="contentEncoding">The content encoding.</param>
+ /// <param name="responseStream">The response stream.</param>
+ internal CachedDirectWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding, MemoryStream responseStream)
+ : base(requestUri, responseUri, headers, statusCode, contentType, contentEncoding) {
+ Requires.NotNull(requestUri, "requestUri");
+ Requires.NotNull(responseStream, "responseStream");
+ this.responseStream = responseStream;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the cached response stream was
+ /// truncated to a maximum allowable length.
+ /// </summary>
+ public bool ResponseTruncated { get; private set; }
+
+ /// <summary>
+ /// Gets the body of the HTTP response.
+ /// </summary>
+ public override Stream ResponseStream {
+ get { return this.responseStream; }
+ }
+
+ /// <summary>
+ /// Gets or sets the cached response stream.
+ /// </summary>
+ internal MemoryStream CachedResponseStream {
+ get { return this.responseStream; }
+ set { this.responseStream = value; }
+ }
+
+ /// <summary>
+ /// Creates a text reader for the response stream.
+ /// </summary>
+ /// <returns>The text reader, initialized for the proper encoding.</returns>
+ public override StreamReader GetResponseReader() {
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding];
+ Encoding encoding = null;
+ if (!string.IsNullOrEmpty(contentEncoding)) {
+ try {
+ encoding = Encoding.GetEncoding(contentEncoding);
+ } catch (ArgumentException ex) {
+ Logger.Messaging.ErrorFormat("Encoding.GetEncoding(\"{0}\") threw ArgumentException: {1}", contentEncoding, ex);
+ }
+ }
+
+ return encoding != null ? new StreamReader(this.ResponseStream, encoding) : new StreamReader(this.ResponseStream);
+ }
+
+ /// <summary>
+ /// Gets the body of the response as a string.
+ /// </summary>
+ /// <returns>The entire body of the response.</returns>
+ internal string GetResponseString() {
+ if (this.ResponseStream != null) {
+ string value = this.GetResponseReader().ReadToEnd();
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ return value;
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets an offline snapshot version of this instance.
+ /// </summary>
+ /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param>
+ /// <returns>A snapshot version of this instance.</returns>
+ /// <remarks>
+ /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot
+ /// will automatically close and dispose of the underlying response stream.
+ /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will
+ /// be the self same instance.
+ /// </remarks>
+ internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) {
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the response to some string, encoded as UTF-8.
+ /// </summary>
+ /// <param name="body">The string to set the response to.</param>
+ internal void SetResponse(string body) {
+ if (body == null) {
+ this.responseStream = null;
+ return;
+ }
+
+ Encoding encoding = Encoding.UTF8;
+ this.Headers[HttpResponseHeader.ContentEncoding] = encoding.HeaderName;
+ this.responseStream = new MemoryStream();
+ StreamWriter writer = new StreamWriter(this.ResponseStream, encoding);
+ writer.Write(body);
+ writer.Flush();
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ }
+
+ /// <summary>
+ /// Caches the network stream and closes it if it is open.
+ /// </summary>
+ /// <param name="response">The response whose stream is to be cloned.</param>
+ /// <param name="maximumBytesToRead">The maximum bytes to cache.</param>
+ /// <returns>The seekable Stream instance that contains a copy of what was returned in the HTTP response.</returns>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Assume(System.Boolean,System.String,System.String)", Justification = "No localization required.")]
+ private static MemoryStream CacheNetworkStreamAndClose(HttpWebResponse response, int maximumBytesToRead) {
+ Requires.NotNull(response, "response");
+ Contract.Ensures(Contract.Result<MemoryStream>() != null);
+
+ // Now read and cache the network stream
+ Stream networkStream = response.GetResponseStream();
+ MemoryStream cachedStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : Math.Min((int)response.ContentLength, maximumBytesToRead));
+ try {
+ Contract.Assume(networkStream.CanRead, "HttpWebResponse.GetResponseStream() always returns a readable stream."); // CC missing
+ Contract.Assume(cachedStream.CanWrite, "This is a MemoryStream -- it's always writable."); // CC missing
+ networkStream.CopyTo(cachedStream);
+ cachedStream.Seek(0, SeekOrigin.Begin);
+
+ networkStream.Dispose();
+ response.Close();
+
+ return cachedStream;
+ } catch {
+ cachedStream.Dispose();
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs
new file mode 100644
index 0000000..f017214
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs
@@ -0,0 +1,1406 @@
+//-----------------------------------------------------------------------
+// <copyright file="Channel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+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;
+
+ /// <summary>
+ /// Manages sending direct messages to a remote party and receiving responses.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable.")]
+ [ContractVerification(true)]
+ [ContractClass(typeof(ChannelContract))]
+ public abstract class Channel : IDisposable {
+ /// <summary>
+ /// The encoding to use when writing out POST entity strings.
+ /// </summary>
+ internal static readonly Encoding PostEntityEncoding = new UTF8Encoding(false);
+
+ /// <summary>
+ /// The content-type used on HTTP POST requests where the POST entity is a
+ /// URL-encoded series of key=value pairs.
+ /// </summary>
+ protected internal const string HttpFormUrlEncoded = "application/x-www-form-urlencoded";
+
+ /// <summary>
+ /// The content-type used for JSON serialized objects.
+ /// </summary>
+ protected internal const string JsonEncoded = "application/json";
+
+ /// <summary>
+ /// The "text/javascript" content-type that some servers return instead of the standard <see cref="JsonEncoded"/> one.
+ /// </summary>
+ protected internal const string JsonTextEncoded = "text/javascript";
+
+ /// <summary>
+ /// The content-type for plain text.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "PlainText", Justification = "Not 'Plaintext' in the crypographic sense.")]
+ protected internal const string PlainTextEncoded = "text/plain";
+
+ /// <summary>
+ /// The content-type used on HTTP POST requests where the POST entity is a
+ /// URL-encoded series of key=value pairs.
+ /// This includes the <see cref="PostEntityEncoding"/> character encoding.
+ /// </summary>
+ protected internal static readonly ContentType HttpFormUrlEncodedContentType = new ContentType(HttpFormUrlEncoded) { CharSet = PostEntityEncoding.WebName };
+
+ /// <summary>
+ /// The HTML that should be returned to the user agent as part of a 301 Redirect.
+ /// </summary>
+ /// <value>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.</value>
+ private const string RedirectResponseBodyFormat = @"<html><head><title>Object moved</title></head><body>
+<h2>Object moved to <a href=""{0}"">here</a>.</h2>
+</body></html>";
+
+ /// <summary>
+ /// A list of binding elements in the order they must be applied to outgoing messages.
+ /// </summary>
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private readonly List<IChannelBindingElement> outgoingBindingElements = new List<IChannelBindingElement>();
+
+ /// <summary>
+ /// A list of binding elements in the order they must be applied to incoming messages.
+ /// </summary>
+ private readonly List<IChannelBindingElement> incomingBindingElements = new List<IChannelBindingElement>();
+
+ /// <summary>
+ /// The template for indirect messages that require form POST to forward through the user agent.
+ /// </summary>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ private const string IndirectMessageFormPostFormat = @"
+<html>
+<head>
+</head>
+<body onload=""document.body.style.display = 'none'; var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; document.getElementById('openid_message').submit()"">
+<form id=""openid_message"" action=""{0}"" method=""post"" accept-charset=""UTF-8"" enctype=""application/x-www-form-urlencoded"" onSubmit=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; return true;"">
+{1}
+ <input id=""submit_button"" type=""submit"" value=""Continue"" />
+</form>
+</body>
+</html>
+";
+
+ /// <summary>
+ /// The default cache of message descriptions to use unless they are customized.
+ /// </summary>
+ /// <remarks>
+ /// This is a perf optimization, so that we don't reflect over every message type
+ /// every time a channel is constructed.
+ /// </remarks>
+ private static MessageDescriptionCollection defaultMessageDescriptions = new MessageDescriptionCollection();
+
+ /// <summary>
+ /// A cache of reflected message types that may be sent or received on this channel.
+ /// </summary>
+ private MessageDescriptionCollection messageDescriptions = defaultMessageDescriptions;
+
+ /// <summary>
+ /// A tool that can figure out what kind of message is being received
+ /// so it can be deserialized.
+ /// </summary>
+ private IMessageFactory messageTypeProvider;
+
+ /// <summary>
+ /// Backing store for the <see cref="CachePolicy"/> property.
+ /// </summary>
+ private RequestCachePolicy cachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
+
+ /// <summary>
+ /// Backing field for the <see cref="MaximumIndirectMessageUrlLength"/> property.
+ /// </summary>
+ private int maximumIndirectMessageUrlLength = Configuration.DotNetOpenAuthSection.Messaging.MaximumIndirectMessageUrlLength;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Channel"/> class.
+ /// </summary>
+ /// <param name="messageTypeProvider">
+ /// A class prepared to analyze incoming messages and indicate what concrete
+ /// message types can deserialize from it.
+ /// </param>
+ /// <param name="bindingElements">The binding elements to use in sending and receiving messages.</param>
+ 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<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements));
+ this.incomingBindingElements = new List<IChannelBindingElement>(this.outgoingBindingElements);
+ this.incomingBindingElements.Reverse();
+
+ foreach (var element in this.outgoingBindingElements) {
+ element.Channel = this;
+ }
+ }
+
+ /// <summary>
+ /// An event fired whenever a message is about to be encoded and sent.
+ /// </summary>
+ internal event EventHandler<ChannelEventArgs> Sending;
+
+ /// <summary>
+ /// Gets or sets an instance to a <see cref="IDirectWebRequestHandler"/> that will be used when
+ /// submitting HTTP requests and waiting for responses.
+ /// </summary>
+ /// <remarks>
+ /// This defaults to a straightforward implementation, but can be set
+ /// to a mock object for testing purposes.
+ /// </remarks>
+ public IDirectWebRequestHandler WebRequestHandler { get; set; }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <value>The default value is 2048.</value>
+ public int MaximumIndirectMessageUrlLength {
+ get {
+ return this.maximumIndirectMessageUrlLength;
+ }
+
+ set {
+ Requires.InRange(value >= 500 && value <= 4096, "value");
+ this.maximumIndirectMessageUrlLength = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the message descriptions.
+ /// </summary>
+ internal virtual MessageDescriptionCollection MessageDescriptions {
+ get {
+ return this.messageDescriptions;
+ }
+
+ set {
+ Requires.NotNull(value, "value");
+ this.messageDescriptions = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a tool that can figure out what kind of message is being received
+ /// so it can be deserialized.
+ /// </summary>
+ internal IMessageFactory MessageFactoryTestHook {
+ get { return this.MessageFactory; }
+ }
+
+ /// <summary>
+ /// Gets the binding elements used by this channel, in no particular guaranteed order.
+ /// </summary>
+ protected internal ReadOnlyCollection<IChannelBindingElement> BindingElements {
+ get {
+ Contract.Ensures(Contract.Result<ReadOnlyCollection<IChannelBindingElement>>() != null);
+ var result = this.outgoingBindingElements.AsReadOnly();
+ Contract.Assume(result != null); // should be an implicit BCL contract
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Gets the binding elements used by this channel, in the order applied to outgoing messages.
+ /// </summary>
+ protected internal ReadOnlyCollection<IChannelBindingElement> OutgoingBindingElements {
+ get { return this.outgoingBindingElements.AsReadOnly(); }
+ }
+
+ /// <summary>
+ /// Gets the binding elements used by this channel, in the order applied to incoming messages.
+ /// </summary>
+ protected internal ReadOnlyCollection<IChannelBindingElement> IncomingBindingElements {
+ get {
+ Contract.Ensures(Contract.Result<ReadOnlyCollection<IChannelBindingElement>>().All(be => be.Channel != null));
+ Contract.Ensures(Contract.Result<ReadOnlyCollection<IChannelBindingElement>>().All(be => be != null));
+ return this.incomingBindingElements.AsReadOnly();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is disposed.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
+ /// </value>
+ protected internal bool IsDisposed { get; set; }
+
+ /// <summary>
+ /// Gets or sets a tool that can figure out what kind of message is being received
+ /// so it can be deserialized.
+ /// </summary>
+ protected virtual IMessageFactory MessageFactory {
+ get { return this.messageTypeProvider; }
+ set { this.messageTypeProvider = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the cache policy to use for direct message requests.
+ /// </summary>
+ /// <value>Default is <see cref="HttpRequestCacheLevel.NoCacheNoStore"/>.</value>
+ protected RequestCachePolicy CachePolicy {
+ get {
+ return this.cachePolicy;
+ }
+
+ set {
+ Requires.NotNull(value, "value");
+ this.cachePolicy = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the XML dictionary reader quotas.
+ /// </summary>
+ /// <value>The XML dictionary reader quotas.</value>
+ protected virtual XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas { get; set; }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="message">The one-way message to send</param>
+ /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception>
+ /// <remarks>
+ /// Requires an HttpContext.Current context.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void Send(IProtocolMessage message) {
+ Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
+ Requires.NotNull(message, "message");
+ this.PrepareResponse(message).Respond(HttpContext.Current, true);
+ }
+
+ /// <summary>
+ /// 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.
+ /// Not safe to call from ASP.NET web forms.
+ /// </summary>
+ /// <param name="message">The one-way message to send</param>
+ /// <remarks>
+ /// Requires an HttpContext.Current context.
+ /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
+ /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
+ /// Use the <see cref="Send"/> method instead for web forms.
+ /// </remarks>
+ public void Respond(IProtocolMessage message) {
+ Requires.ValidState(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired);
+ Requires.NotNull(message, "message");
+ this.PrepareResponse(message).Respond();
+ }
+
+ /// <summary>
+ /// Prepares an indirect message (either a request or response)
+ /// or direct message response for transmission to a remote party.
+ /// </summary>
+ /// <param name="message">The one-way message to send</param>
+ /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns>
+ public OutgoingWebResponse PrepareResponse(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != 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;
+ }
+
+ /// <summary>
+ /// Gets the protocol message embedded in the given HTTP request, if present.
+ /// </summary>
+ /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
+ /// <remarks>
+ /// Requires an HttpContext.Current context.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception>
+ public IDirectedProtocolMessage ReadFromRequest() {
+ return this.ReadFromRequest(this.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets the protocol message embedded in the given HTTP request, if present.
+ /// </summary>
+ /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam>
+ /// <param name="request">The deserialized message, if one is found. Null otherwise.</param>
+ /// <returns>True if the expected message was recognized and deserialized. False otherwise.</returns>
+ /// <remarks>
+ /// Requires an HttpContext.Current context.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception>
+ /// <exception cref="ProtocolException">Thrown when a request message of an unexpected type is received.</exception>
+ public bool TryReadFromRequest<TRequest>(out TRequest request)
+ where TRequest : class, IProtocolMessage {
+ return TryReadFromRequest<TRequest>(this.GetRequestFromContext(), out request);
+ }
+
+ /// <summary>
+ /// Gets the protocol message embedded in the given HTTP request, if present.
+ /// </summary>
+ /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam>
+ /// <param name="httpRequest">The request to search for an embedded message.</param>
+ /// <param name="request">The deserialized message, if one is found. Null otherwise.</param>
+ /// <returns>True if the expected message was recognized and deserialized. False otherwise.</returns>
+ /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception>
+ /// <exception cref="ProtocolException">Thrown when a request message of an unexpected type is received.</exception>
+ public bool TryReadFromRequest<TRequest>(HttpRequestInfo httpRequest, out TRequest request)
+ where TRequest : class, IProtocolMessage {
+ Requires.NotNull(httpRequest, "httpRequest");
+ Contract.Ensures(Contract.Result<bool>() == (Contract.ValueAtReturn<TRequest>(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;
+ }
+
+ /// <summary>
+ /// Gets the protocol message embedded in the current HTTP request.
+ /// </summary>
+ /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam>
+ /// <returns>The deserialized message. Never null.</returns>
+ /// <remarks>
+ /// Requires an HttpContext.Current context.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown when <see cref="HttpContext.Current"/> is null.</exception>
+ /// <exception cref="ProtocolException">Thrown if the expected message was not recognized in the response.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")]
+ public TRequest ReadFromRequest<TRequest>()
+ where TRequest : class, IProtocolMessage {
+ return this.ReadFromRequest<TRequest>(this.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets the protocol message embedded in the given HTTP request.
+ /// </summary>
+ /// <typeparam name="TRequest">The expected type of the message to be received.</typeparam>
+ /// <param name="httpRequest">The request to search for an embedded message.</param>
+ /// <returns>The deserialized message. Never null.</returns>
+ /// <exception cref="ProtocolException">Thrown if the expected message was not recognized in the response.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")]
+ public TRequest ReadFromRequest<TRequest>(HttpRequestInfo httpRequest)
+ where TRequest : class, IProtocolMessage {
+ Requires.NotNull(httpRequest, "httpRequest");
+ TRequest request;
+ if (this.TryReadFromRequest<TRequest>(httpRequest, out request)) {
+ return request;
+ } else {
+ throw ErrorUtilities.ThrowProtocol(MessagingStrings.ExpectedMessageNotReceived, typeof(TRequest));
+ }
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be embedded in the given HTTP request.
+ /// </summary>
+ /// <param name="httpRequest">The request to search for an embedded message.</param>
+ /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Sends a direct message to a remote party and waits for the response.
+ /// </summary>
+ /// <typeparam name="TResponse">The expected type of the message to be received.</typeparam>
+ /// <param name="requestMessage">The message to send.</param>
+ /// <returns>The remote party's response.</returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown if no message is recognized in the response
+ /// or an unexpected type of message is received.
+ /// </exception>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This returns and verifies the appropriate message type.")]
+ public TResponse Request<TResponse>(IDirectedProtocolMessage requestMessage)
+ where TResponse : class, IProtocolMessage {
+ Requires.NotNull(requestMessage, "requestMessage");
+ Contract.Ensures(Contract.Result<TResponse>() != 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;
+ }
+
+ /// <summary>
+ /// Sends a direct message to a remote party and waits for the response.
+ /// </summary>
+ /// <param name="requestMessage">The message to send.</param>
+ /// <returns>The remote party's response. Guaranteed to never be null.</returns>
+ /// <exception cref="ProtocolException">Thrown if the response does not include a protocol message.</exception>
+ 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
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Verifies the integrity and applicability of an incoming message.
+ /// </summary>
+ /// <param name="message">The message just received.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the message is somehow invalid.
+ /// This can be due to tampering, replay attack or expiration, among other things.
+ /// </exception>
+ internal void ProcessIncomingMessageTestHook(IProtocolMessage message) {
+ this.ProcessIncomingMessage(message);
+ }
+
+ /// <summary>
+ /// Prepares an HTTP request that carries a given message.
+ /// </summary>
+ /// <param name="request">The message to send.</param>
+ /// <returns>The <see cref="HttpWebRequest"/> prepared to send the request.</returns>
+ /// <remarks>
+ /// This method must be overridden by a derived class, unless the <see cref="RequestCore"/> method
+ /// is overridden and does not require this method.
+ /// </remarks>
+ internal HttpWebRequest CreateHttpRequestTestHook(IDirectedProtocolMessage request) {
+ return this.CreateHttpRequest(request);
+ }
+
+ /// <summary>
+ /// Queues a message for sending in the response stream where the fields
+ /// are sent in the response stream in querystring style.
+ /// </summary>
+ /// <param name="response">The message to send as a response.</param>
+ /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns>
+ /// <remarks>
+ /// This method implements spec OAuth V1.0 section 5.3.
+ /// </remarks>
+ internal OutgoingWebResponse PrepareDirectResponseTestHook(IProtocolMessage response) {
+ return this.PrepareDirectResponse(response);
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be in the given HTTP response.
+ /// </summary>
+ /// <param name="response">The response that is anticipated to contain an protocol message.</param>
+ /// <returns>The deserialized message parts, if found. Null otherwise.</returns>
+ /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception>
+ internal IDictionary<string, string> ReadFromResponseCoreTestHook(IncomingWebResponse response) {
+ return this.ReadFromResponseCore(response);
+ }
+
+ /// <remarks>
+ /// This method should NOT be called by derived types
+ /// except when sending ONE WAY request messages.
+ /// </remarks>
+ /// <summary>
+ /// Prepares a message for transmit by applying signatures, nonces, etc.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ internal void ProcessOutgoingMessageTestHook(IProtocolMessage message) {
+ this.ProcessOutgoingMessage(message);
+ }
+
+ /// <summary>
+ /// Gets the current HTTP request being processed.
+ /// </summary>
+ /// <returns>The HttpRequestInfo for the current request.</returns>
+ /// <remarks>
+ /// Requires an <see cref="HttpContext.Current"/> context.
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ [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<HttpRequestInfo>() != null);
+ Contract.Ensures(Contract.Result<HttpRequestInfo>().Url != null);
+ Contract.Ensures(Contract.Result<HttpRequestInfo>().RawUrl != null);
+ Contract.Ensures(Contract.Result<HttpRequestInfo>().UrlBeforeRewriting != null);
+
+ Contract.Assume(HttpContext.Current.Request.Url != null);
+ Contract.Assume(HttpContext.Current.Request.RawUrl != null);
+ return new HttpRequestInfo(HttpContext.Current.Request);
+ }
+
+ /// <summary>
+ /// Checks whether a given HTTP method is expected to include an entity body in its request.
+ /// </summary>
+ /// <param name="httpMethod">The HTTP method.</param>
+ /// <returns><c>true</c> if the HTTP method is supposed to have an entity; <c>false</c> otherwise.</returns>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ // Call dispose on any binding elements that need it.
+ foreach (IDisposable bindingElement in this.BindingElements.OfType<IDisposable>()) {
+ bindingElement.Dispose();
+ }
+
+ this.IsDisposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="Sending"/> event.
+ /// </summary>
+ /// <param name="message">The message about to be encoded and sent.</param>
+ protected virtual void OnSending(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+
+ var sending = this.Sending;
+ if (sending != null) {
+ sending(this, new ChannelEventArgs(message));
+ }
+ }
+
+ /// <summary>
+ /// Gets the direct response of a direct HTTP request.
+ /// </summary>
+ /// <param name="webRequest">The web request.</param>
+ /// <returns>The response to the web request.</returns>
+ /// <exception cref="ProtocolException">Thrown on network or protocol errors.</exception>
+ protected virtual IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) {
+ Requires.NotNull(webRequest, "webRequest");
+ return this.WebRequestHandler.GetResponse(webRequest);
+ }
+
+ /// <summary>
+ /// Submits a direct request message to some remote party and blocks waiting for an immediately reply.
+ /// </summary>
+ /// <param name="request">The request message.</param>
+ /// <returns>The response message, or null if the response did not carry a message.</returns>
+ /// <remarks>
+ /// Typically a deriving channel will override <see cref="CreateHttpRequest"/> 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.
+ /// </remarks>
+ protected virtual IProtocolMessage RequestCore(IDirectedProtocolMessage request) {
+ Requires.NotNull(request, "request");
+ Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
+
+ HttpWebRequest webRequest = this.CreateHttpRequest(request);
+ IDictionary<string, string> 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;
+ }
+
+ /// <summary>
+ /// Called when receiving a direct response message, before deserialization begins.
+ /// </summary>
+ /// <param name="response">The HTTP direct response.</param>
+ /// <param name="message">The newly instantiated message, prior to deserialization.</param>
+ protected virtual void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) {
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be embedded in the given HTTP request.
+ /// </summary>
+ /// <param name="request">The request to search for an embedded message.</param>
+ /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
+ 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);
+ }
+
+ /// <summary>
+ /// Deserializes a dictionary of values into a message.
+ /// </summary>
+ /// <param name="fields">The dictionary of values that were read from an HTTP request or response.</param>
+ /// <param name="recipient">Information about where the message was directed. Null for direct response messages.</param>
+ /// <returns>The deserialized message, or null if no message could be recognized in the provided data.</returns>
+ protected virtual IProtocolMessage Receive(Dictionary<string, string> 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;
+ }
+
+ /// <summary>
+ /// Queues an indirect message for transmittal via the user agent.
+ /// </summary>
+ /// <param name="message">The message to send.</param>
+ /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns>
+ 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<OutgoingWebResponse>() != 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;
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="message">The message to forward.</param>
+ /// <param name="fields">The pre-serialized fields from the message.</param>
+ /// <param name="payloadInFragment">if set to <c>true</c> the redirect will contain the message payload in the #fragment portion of the URL rather than the ?querystring.</param>
+ /// <returns>The encoded HTTP response.</returns>
+ [Pure]
+ protected virtual OutgoingWebResponse Create301RedirectResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields, bool payloadInFragment = false) {
+ Requires.NotNull(message, "message");
+ Requires.True(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
+ Requires.NotNull(fields, "fields");
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != 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;
+ }
+
+ /// <summary>
+ /// Encodes an HTTP response that will instruct the user agent to forward a message to
+ /// some remote third party using a form POST method.
+ /// </summary>
+ /// <param name="message">The message to forward.</param>
+ /// <param name="fields">The pre-serialized fields from the message.</param>
+ /// <returns>The encoded HTTP response.</returns>
+ protected virtual OutgoingWebResponse CreateFormPostResponse(IDirectedProtocolMessage message, IDictionary<string, string> fields) {
+ Requires.NotNull(message, "message");
+ Requires.True(message.Recipient != null, "message", MessagingStrings.DirectedMessageMissingRecipient);
+ Requires.NotNull(fields, "fields");
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != 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<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\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;
+ }
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be in the given HTTP response.
+ /// </summary>
+ /// <param name="response">The response that is anticipated to contain an protocol message.</param>
+ /// <returns>The deserialized message parts, if found. Null otherwise.</returns>
+ /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception>
+ protected abstract IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response);
+
+ /// <summary>
+ /// Prepares an HTTP request that carries a given message.
+ /// </summary>
+ /// <param name="request">The message to send.</param>
+ /// <returns>The <see cref="HttpWebRequest"/> prepared to send the request.</returns>
+ /// <remarks>
+ /// This method must be overridden by a derived class, unless the <see cref="Channel.RequestCore"/> method
+ /// is overridden and does not require this method.
+ /// </remarks>
+ protected virtual HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) {
+ Requires.NotNull(request, "request");
+ Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient);
+ Contract.Ensures(Contract.Result<HttpWebRequest>() != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Queues a message for sending in the response stream where the fields
+ /// are sent in the response stream in querystring style.
+ /// </summary>
+ /// <param name="response">The message to send as a response.</param>
+ /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns>
+ /// <remarks>
+ /// This method implements spec OAuth V1.0 section 5.3.
+ /// </remarks>
+ protected abstract OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response);
+
+ /// <summary>
+ /// Serializes the given message as a JSON string.
+ /// </summary>
+ /// <param name="message">The message to serialize.</param>
+ /// <returns>A JSON string.</returns>
+ [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;
+ }
+ }
+
+ /// <summary>
+ /// Deserializes from flat data from a JSON object.
+ /// </summary>
+ /// <param name="json">A JSON string.</param>
+ /// <returns>The simple "key":"value" pairs from a JSON-encoded object.</returns>
+ protected virtual IDictionary<string, string> DeserializeFromJson(string json) {
+ Requires.NotNullOrEmpty(json, "json");
+
+ var dictionary = new Dictionary<string, string>();
+ using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(json), this.XmlDictionaryReaderQuotas)) {
+ MessageSerializer.DeserializeJsonAsFlatDictionary(dictionary, jsonReader);
+ }
+ return dictionary;
+ }
+
+ /// <summary>
+ /// Prepares a message for transmit by applying signatures, nonces, etc.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <remarks>
+ /// This method should NOT be called by derived types
+ /// except when sending ONE WAY request messages.
+ /// </remarks>
+ 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 : "<response>";
+ 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());
+ }
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider as the query string in a GET request.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ 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;
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider as the query string in a HEAD request.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ 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;
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider as the payload of a POST request.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ protected virtual HttpWebRequest InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) {
+ Requires.NotNull(requestMessage, "requestMessage");
+ Contract.Ensures(Contract.Result<HttpWebRequest>() != 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<MultipartPostPart>(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;
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider as the query string in a PUT request.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// This method is simply a standard HTTP PUT request with the message parts serialized to the query string.
+ /// </remarks>
+ protected virtual HttpWebRequest InitializeRequestAsPut(IDirectedProtocolMessage requestMessage) {
+ Requires.NotNull(requestMessage, "requestMessage");
+ Contract.Ensures(Contract.Result<HttpWebRequest>() != null);
+
+ HttpWebRequest request = this.InitializeRequestAsGet(requestMessage);
+ request.Method = "PUT";
+ return request;
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider as the query string in a DELETE request.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// This method is simply a standard HTTP DELETE request with the message parts serialized to the query string.
+ /// </remarks>
+ protected virtual HttpWebRequest InitializeRequestAsDelete(IDirectedProtocolMessage requestMessage) {
+ Requires.NotNull(requestMessage, "requestMessage");
+ Contract.Ensures(Contract.Result<HttpWebRequest>() != null);
+
+ HttpWebRequest request = this.InitializeRequestAsGet(requestMessage);
+ request.Method = "DELETE";
+ return request;
+ }
+
+ /// <summary>
+ /// Sends the given parameters in the entity stream of an HTTP request.
+ /// </summary>
+ /// <param name="httpRequest">The HTTP request.</param>
+ /// <param name="fields">The parameters to send.</param>
+ /// <remarks>
+ /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes
+ /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>.
+ /// </remarks>
+ protected void SendParametersInEntity(HttpWebRequest httpRequest, IDictionary<string, string> 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();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Sends the given parameters in the entity stream of an HTTP request in multi-part format.
+ /// </summary>
+ /// <param name="httpRequest">The HTTP request.</param>
+ /// <param name="fields">The parameters to send.</param>
+ /// <remarks>
+ /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes
+ /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>.
+ /// </remarks>
+ protected void SendParametersInEntityAsMultipart(HttpWebRequest httpRequest, IEnumerable<MultipartPostPart> fields) {
+ httpRequest.PostMultipartNoGetResponse(this.WebRequestHandler, fields);
+ }
+
+ /// <summary>
+ /// Verifies the integrity and applicability of an incoming message.
+ /// </summary>
+ /// <param name="message">The message just received.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the message is somehow invalid.
+ /// This can be due to tampering, replay attack or expiration, among other things.
+ /// </exception>
+ 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();
+ }
+
+ /// <summary>
+ /// Allows preprocessing and validation of message data before an appropriate message type is
+ /// selected or deserialized.
+ /// </summary>
+ /// <param name="fields">The received message data.</param>
+ protected virtual void FilterReceivedFields(IDictionary<string, string> fields) {
+ }
+
+ /// <summary>
+ /// Customizes the binding element order for outgoing and incoming messages.
+ /// </summary>
+ /// <param name="outgoingOrder">The outgoing order.</param>
+ /// <param name="incomingOrder">The incoming order.</param>
+ /// <remarks>
+ /// No binding elements can be added or removed from the channel using this method.
+ /// Only a customized order is allowed.
+ /// </remarks>
+ /// <exception cref="ArgumentException">Thrown if a binding element is new or missing in one of the ordered lists.</exception>
+ protected void CustomizeBindingElementOrder(IEnumerable<IChannelBindingElement> outgoingOrder, IEnumerable<IChannelBindingElement> 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);
+ }
+
+ /// <summary>
+ /// Ensures a consistent and secure set of binding elements and
+ /// sorts them as necessary for a valid sequence of operations.
+ /// </summary>
+ /// <param name="elements">The binding elements provided to the channel.</param>
+ /// <returns>The properly ordered list of elements.</returns>
+ /// <exception cref="ProtocolException">Thrown when the binding elements are incomplete or inconsistent with each other.</exception>
+ private static IEnumerable<IChannelBindingElement> ValidateAndPrepareBindingElements(IEnumerable<IChannelBindingElement> elements) {
+ Requires.NullOrWithNoNullElements(elements, "elements");
+ Contract.Ensures(Contract.Result<IEnumerable<IChannelBindingElement>>() != null);
+ if (elements == null) {
+ return new IChannelBindingElement[0];
+ }
+
+ // Filter the elements between the mere transforming ones and the protection ones.
+ var transformationElements = new List<IChannelBindingElement>(
+ elements.Where(element => element.Protection == MessageProtections.None));
+ var protectionElements = new List<IChannelBindingElement>(
+ 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<IChannelBindingElement>(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;
+ }
+
+ /// <summary>
+ /// Puts binding elements in their correct outgoing message processing order.
+ /// </summary>
+ /// <param name="protection1">The first protection type to compare.</param>
+ /// <param name="protection2">The second protection type to compare.</param>
+ /// <returns>
+ /// -1 if <paramref name="protection1"/> should be applied to an outgoing message before <paramref name="protection2"/>.
+ /// 1 if <paramref name="protection2"/> should be applied to an outgoing message before <paramref name="protection1"/>.
+ /// 0 if it doesn't matter.
+ /// </returns>
+ 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
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [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
+
+ /// <summary>
+ /// Verifies that all required message parts are initialized to values
+ /// prior to sending the message to a remote party.
+ /// </summary>
+ /// <param name="message">The message to verify.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when any required message part does not have a value.
+ /// </exception>
+ private void EnsureValidMessageParts(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ MessageDictionary dictionary = this.MessageDescriptions.GetAccessor(message);
+ MessageDescription description = this.MessageDescriptions.Get(message);
+ description.EnsureMessagePartsPassBasicValidation(dictionary);
+ }
+
+ /// <summary>
+ /// Determines whether a given ordered list of binding elements includes every
+ /// binding element in this channel exactly once.
+ /// </summary>
+ /// <param name="order">The list of binding elements to test.</param>
+ /// <returns>
+ /// <c>true</c> if the given list is a valid description of a binding element ordering; otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ private bool IsBindingElementOrderValid(IEnumerable<IChannelBindingElement> 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;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/ChannelContract.cs b/src/DotNetOpenAuth.Core/Messaging/ChannelContract.cs
new file mode 100644
index 0000000..bf313ef
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/ChannelContract.cs
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------
+// <copyright file="ChannelContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Code contract for the <see cref="Channel"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(Channel))]
+ internal abstract class ChannelContract : Channel {
+ /// <summary>
+ /// Prevents a default instance of the ChannelContract class from being created.
+ /// </summary>
+ private ChannelContract()
+ : base(null, null) {
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be in the given HTTP response.
+ /// </summary>
+ /// <param name="response">The response that is anticipated to contain an protocol message.</param>
+ /// <returns>
+ /// The deserialized message parts, if found. Null otherwise.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception>
+ protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) {
+ Requires.NotNull(response, "response");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Queues a message for sending in the response stream where the fields
+ /// are sent in the response stream in querystring style.
+ /// </summary>
+ /// <param name="response">The message to send as a response.</param>
+ /// <returns>
+ /// The pending user agent redirect based message to be sent as an HttpResponse.
+ /// </returns>
+ /// <remarks>
+ /// This method implements spec V1.0 section 5.3.
+ /// </remarks>
+ protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) {
+ Requires.NotNull(response, "response");
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/ChannelEventArgs.cs b/src/DotNetOpenAuth.Core/Messaging/ChannelEventArgs.cs
new file mode 100644
index 0000000..e09e655
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/ChannelEventArgs.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+// <copyright file="ChannelEventArgs.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// The data packet sent with Channel events.
+ /// </summary>
+ public class ChannelEventArgs : EventArgs {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelEventArgs"/> class.
+ /// </summary>
+ /// <param name="message">The message behind the fired event..</param>
+ internal ChannelEventArgs(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+
+ this.Message = message;
+ }
+
+ /// <summary>
+ /// Gets the message that caused the event to fire.
+ /// </summary>
+ public IProtocolMessage Message { get; private set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBag.cs b/src/DotNetOpenAuth.Core/Messaging/DataBag.cs
new file mode 100644
index 0000000..17a7bda
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/DataBag.cs
@@ -0,0 +1,147 @@
+//-----------------------------------------------------------------------
+// <copyright file="DataBag.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A collection of message parts that will be serialized into a single string,
+ /// to be set into a larger message.
+ /// </summary>
+ internal abstract class DataBag : IMessage {
+ /// <summary>
+ /// The default version for DataBags.
+ /// </summary>
+ private static readonly Version DefaultVersion = new Version(1, 0);
+
+ /// <summary>
+ /// The backing field for the <see cref="IMessage.Version"/> property.
+ /// </summary>
+ private Version version;
+
+ /// <summary>
+ /// A dictionary to contain extra message data.
+ /// </summary>
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DataBag"/> class.
+ /// </summary>
+ protected DataBag()
+ : this(DefaultVersion) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DataBag"/> class.
+ /// </summary>
+ /// <param name="version">The DataBag version.</param>
+ protected DataBag(Version version) {
+ Contract.Requires(version != null);
+ this.version = version;
+ }
+
+ #region IMessage Properties
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ Version IMessage.Version {
+ get { return this.version; }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ public IDictionary<string, string> ExtraData {
+ get { return this.extraData; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets the nonce.
+ /// </summary>
+ /// <value>The nonce.</value>
+ [MessagePart]
+ internal byte[] Nonce { get; set; }
+
+ /// <summary>
+ /// Gets or sets the UTC creation date of this token.
+ /// </summary>
+ /// <value>The UTC creation date.</value>
+ [MessagePart("ts", IsRequired = true, Encoder = typeof(TimestampEncoder))]
+ internal DateTime UtcCreationDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the signature.
+ /// </summary>
+ /// <value>The signature.</value>
+ internal byte[] Signature { get; set; }
+
+ /// <summary>
+ /// Gets or sets the message that delivered this DataBag instance to this host.
+ /// </summary>
+ protected internal IProtocolMessage ContainingMessage { get; set; }
+
+ /// <summary>
+ /// Gets the type of this instance.
+ /// </summary>
+ /// <value>The type of the bag.</value>
+ /// <remarks>
+ /// This ensures that one token cannot be misused as another kind of token.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Accessed by reflection")]
+ [MessagePart("t", IsRequired = true, AllowEmpty = false)]
+ private Type BagType {
+ get { return this.GetType(); }
+ }
+
+ #region IMessage Methods
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void IMessage.EnsureValidMessage() {
+ this.EnsureValidMessage();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ protected virtual void EnsureValidMessage() {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs
new file mode 100644
index 0000000..86ada44
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs
@@ -0,0 +1,354 @@
+//-----------------------------------------------------------------------
+// <copyright file="DataBagFormatterBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A serializer for <see cref="DataBag"/>-derived types
+ /// </summary>
+ /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam>
+ internal abstract class DataBagFormatterBase<T> : IDataBagFormatter<T> where T : DataBag, new() {
+ /// <summary>
+ /// The message description cache to use for data bag types.
+ /// </summary>
+ protected static readonly MessageDescriptionCollection MessageDescriptions = new MessageDescriptionCollection();
+
+ /// <summary>
+ /// The length of the nonce to include in tokens that can be decoded once only.
+ /// </summary>
+ private const int NonceLength = 6;
+
+ /// <summary>
+ /// The minimum allowable lifetime for the key used to encrypt/decrypt or sign this databag.
+ /// </summary>
+ private readonly TimeSpan minimumAge = TimeSpan.FromDays(1);
+
+ /// <summary>
+ /// The symmetric key store with the secret used for signing/encryption of verification codes and refresh tokens.
+ /// </summary>
+ private readonly ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// The bucket for symmetric keys.
+ /// </summary>
+ private readonly string cryptoKeyBucket;
+
+ /// <summary>
+ /// The crypto to use for signing access tokens.
+ /// </summary>
+ private readonly RSACryptoServiceProvider asymmetricSigning;
+
+ /// <summary>
+ /// The crypto to use for encrypting access tokens.
+ /// </summary>
+ private readonly RSACryptoServiceProvider asymmetricEncrypting;
+
+ /// <summary>
+ /// A value indicating whether the data in this instance will be protected against tampering.
+ /// </summary>
+ private readonly bool signed;
+
+ /// <summary>
+ /// The nonce store to use to ensure that this instance is only decoded once.
+ /// </summary>
+ private readonly INonceStore decodeOnceOnly;
+
+ /// <summary>
+ /// The maximum age of a token that can be decoded; useful only when <see cref="decodeOnceOnly"/> is <c>true</c>.
+ /// </summary>
+ private readonly TimeSpan? maximumAge;
+
+ /// <summary>
+ /// A value indicating whether the data in this instance will be protected against eavesdropping.
+ /// </summary>
+ private readonly bool encrypted;
+
+ /// <summary>
+ /// A value indicating whether the data in this instance will be GZip'd.
+ /// </summary>
+ private readonly bool compressed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DataBagFormatterBase&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="signingKey">The crypto service provider with the asymmetric key to use for signing or verifying the token.</param>
+ /// <param name="encryptingKey">The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected DataBagFormatterBase(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : this(signingKey != null, encryptingKey != null, compressed, maximumAge, decodeOnceOnly) {
+ this.asymmetricSigning = signingKey;
+ this.asymmetricEncrypting = encryptingKey;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DataBagFormatterBase&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
+ /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
+ /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The required minimum lifespan within which this token must be decodable and verifiable; useful only when <paramref name="signed"/> and/or <paramref name="encrypted"/> is true.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected DataBagFormatterBase(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : this(signed, encrypted, compressed, maximumAge, decodeOnceOnly) {
+ Requires.True(!String.IsNullOrEmpty(bucket) || cryptoKeyStore == null, null);
+ Requires.True(cryptoKeyStore != null || (!signed && !encrypted), null);
+
+ this.cryptoKeyStore = cryptoKeyStore;
+ this.cryptoKeyBucket = bucket;
+ if (minimumAge.HasValue) {
+ this.minimumAge = minimumAge.Value;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DataBagFormatterBase&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
+ /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ private DataBagFormatterBase(bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) {
+ Requires.True(signed || decodeOnceOnly == null, null);
+ Requires.True(maximumAge.HasValue || decodeOnceOnly == null, null);
+
+ this.signed = signed;
+ this.maximumAge = maximumAge;
+ this.decodeOnceOnly = decodeOnceOnly;
+ this.encrypted = encrypted;
+ this.compressed = compressed;
+ }
+
+ /// <summary>
+ /// Serializes the specified message, including compression, encryption, signing, and nonce handling where applicable.
+ /// </summary>
+ /// <param name="message">The message to serialize. Must not be null.</param>
+ /// <returns>A non-null, non-empty value.</returns>
+ public string Serialize(T message) {
+ message.UtcCreationDate = DateTime.UtcNow;
+
+ if (this.decodeOnceOnly != null) {
+ message.Nonce = MessagingUtilities.GetNonCryptoRandomData(NonceLength);
+ }
+
+ byte[] encoded = this.SerializeCore(message);
+
+ if (this.compressed) {
+ encoded = MessagingUtilities.Compress(encoded);
+ }
+
+ string symmetricSecretHandle = null;
+ if (this.encrypted) {
+ encoded = this.Encrypt(encoded, out symmetricSecretHandle);
+ }
+
+ if (this.signed) {
+ message.Signature = this.CalculateSignature(encoded, symmetricSecretHandle);
+ }
+
+ int capacity = this.signed ? 4 + message.Signature.Length + 4 + encoded.Length : encoded.Length;
+ using (var finalStream = new MemoryStream(capacity)) {
+ var writer = new BinaryWriter(finalStream);
+ if (this.signed) {
+ writer.WriteBuffer(message.Signature);
+ }
+
+ writer.WriteBuffer(encoded);
+ writer.Flush();
+
+ string payload = MessagingUtilities.ConvertToBase64WebSafeString(finalStream.ToArray());
+ string result = payload;
+ if (symmetricSecretHandle != null && (this.signed || this.encrypted)) {
+ result = MessagingUtilities.CombineKeyHandleAndPayload(symmetricSecretHandle, payload);
+ }
+
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Deserializes a <see cref="DataBag"/>, including decompression, decryption, signature and nonce validation where applicable.
+ /// </summary>
+ /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param>
+ /// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param>
+ /// <returns>The deserialized value. Never null.</returns>
+ public T Deserialize(IProtocolMessage containingMessage, string value) {
+ string symmetricSecretHandle = null;
+ if (this.encrypted && this.cryptoKeyStore != null) {
+ string valueWithoutHandle;
+ MessagingUtilities.ExtractKeyHandleAndPayload(containingMessage, "<TODO>", value, out symmetricSecretHandle, out valueWithoutHandle);
+ value = valueWithoutHandle;
+ }
+
+ var message = new T { ContainingMessage = containingMessage };
+ byte[] data = MessagingUtilities.FromBase64WebSafeString(value);
+
+ byte[] signature = null;
+ if (this.signed) {
+ using (var dataStream = new MemoryStream(data)) {
+ var dataReader = new BinaryReader(dataStream);
+ signature = dataReader.ReadBuffer();
+ data = dataReader.ReadBuffer();
+ }
+
+ // Verify that the verification code was issued by message authorization server.
+ ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature, symmetricSecretHandle), MessagingStrings.SignatureInvalid);
+ }
+
+ if (this.encrypted) {
+ data = this.Decrypt(data, symmetricSecretHandle);
+ }
+
+ if (this.compressed) {
+ data = MessagingUtilities.Decompress(data);
+ }
+
+ this.DeserializeCore(message, data);
+ message.Signature = signature; // TODO: we don't really need this any more, do we?
+
+ if (this.maximumAge.HasValue) {
+ // Has message verification code expired?
+ DateTime expirationDate = message.UtcCreationDate + this.maximumAge.Value;
+ if (expirationDate < DateTime.UtcNow) {
+ throw new ExpiredMessageException(expirationDate, containingMessage);
+ }
+ }
+
+ // Has message verification code already been used to obtain an access/refresh token?
+ if (this.decodeOnceOnly != null) {
+ ErrorUtilities.VerifyInternal(this.maximumAge.HasValue, "Oops! How can we validate a nonce without a maximum message age?");
+ string context = "{" + GetType().FullName + "}";
+ if (!this.decodeOnceOnly.StoreNonce(context, Convert.ToBase64String(message.Nonce), message.UtcCreationDate)) {
+ Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", message.Nonce, message.UtcCreationDate);
+ throw new ReplayedMessageException(containingMessage);
+ }
+ }
+
+ ((IMessage)message).EnsureValidMessage();
+
+ return message;
+ }
+
+ /// <summary>
+ /// Serializes the <see cref="DataBag"/> instance to a buffer.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>The buffer containing the serialized data.</returns>
+ protected abstract byte[] SerializeCore(T message);
+
+ /// <summary>
+ /// Deserializes the <see cref="DataBag"/> instance from a buffer.
+ /// </summary>
+ /// <param name="message">The message instance to initialize with data from the buffer.</param>
+ /// <param name="data">The data buffer.</param>
+ protected abstract void DeserializeCore(T message, byte[] data);
+
+ /// <summary>
+ /// Determines whether the signature on this instance is valid.
+ /// </summary>
+ /// <param name="signedData">The signed data.</param>
+ /// <param name="signature">The signature.</param>
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// <c>true</c> if the signature is valid; otherwise, <c>false</c>.
+ /// </returns>
+ private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) {
+ Requires.NotNull(signedData, "signedData");
+ Requires.NotNull(signature, "signature");
+
+ if (this.asymmetricSigning != null) {
+ using (var hasher = new SHA1CryptoServiceProvider()) {
+ return this.asymmetricSigning.VerifyData(signedData, hasher, signature);
+ }
+ } else {
+ return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle));
+ }
+ }
+
+ /// <summary>
+ /// Calculates the signature for the data in this verification code.
+ /// </summary>
+ /// <param name="bytesToSign">The bytes to sign.</param>
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// The calculated signature.
+ /// </returns>
+ private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) {
+ Requires.NotNull(bytesToSign, "bytesToSign");
+ Requires.ValidState(this.asymmetricSigning != null || this.cryptoKeyStore != null);
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+
+ if (this.asymmetricSigning != null) {
+ using (var hasher = new SHA1CryptoServiceProvider()) {
+ return this.asymmetricSigning.SignData(bytesToSign, hasher);
+ }
+ } else {
+ var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
+ ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
+ using (var symmetricHasher = new HMACSHA256(key.Key)) {
+ return symmetricHasher.ComputeHash(bytesToSign);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Encrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <param name="symmetricSecretHandle">Receives the symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// The encrypted value.
+ /// </returns>
+ private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) {
+ Requires.ValidState(this.asymmetricEncrypting != null || this.cryptoKeyStore != null);
+
+ if (this.asymmetricEncrypting != null) {
+ symmetricSecretHandle = null;
+ return this.asymmetricEncrypting.EncryptWithRandomSymmetricKey(value);
+ } else {
+ var cryptoKey = this.cryptoKeyStore.GetCurrentKey(this.cryptoKeyBucket, this.minimumAge);
+ symmetricSecretHandle = cryptoKey.Key;
+ return MessagingUtilities.Encrypt(value, cryptoKey.Value.Key);
+ }
+ }
+
+ /// <summary>
+ /// Decrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
+ /// <returns>
+ /// The decrypted value.
+ /// </returns>
+ private byte[] Decrypt(byte[] value, string symmetricSecretHandle) {
+ Requires.ValidState(this.asymmetricEncrypting != null || symmetricSecretHandle != null);
+
+ if (this.asymmetricEncrypting != null) {
+ return this.asymmetricEncrypting.DecryptWithRandomSymmetricKey(value);
+ } else {
+ var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
+ ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
+ return MessagingUtilities.Decrypt(value, key.Key);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/DirectWebRequestOptions.cs b/src/DotNetOpenAuth.Core/Messaging/DirectWebRequestOptions.cs
new file mode 100644
index 0000000..f3ce805
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/DirectWebRequestOptions.cs
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------
+// <copyright file="DirectWebRequestOptions.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Net;
+
+ /// <summary>
+ /// A set of flags that can control the behavior of an individual web request.
+ /// </summary>
+ [Flags]
+ public enum DirectWebRequestOptions {
+ /// <summary>
+ /// Indicates that default <see cref="HttpWebRequest"/> behavior is required.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// Indicates that any response from the remote server, even those
+ /// with HTTP status codes that indicate errors, should not result
+ /// in a thrown exception.
+ /// </summary>
+ /// <remarks>
+ /// Even with this flag set, <see cref="ProtocolException"/> should
+ /// be thrown when an HTTP protocol error occurs (i.e. timeouts).
+ /// </remarks>
+ AcceptAllHttpResponses = 0x1,
+
+ /// <summary>
+ /// Indicates that the HTTP request must be completed entirely
+ /// using SSL (including any redirects).
+ /// </summary>
+ RequireSsl = 0x2,
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/EmptyDictionary.cs b/src/DotNetOpenAuth.Core/Messaging/EmptyDictionary.cs
new file mode 100644
index 0000000..9db5169
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/EmptyDictionary.cs
@@ -0,0 +1,250 @@
+//-----------------------------------------------------------------------
+// <copyright file="EmptyDictionary.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+
+ /// <summary>
+ /// An empty dictionary. Useful for avoiding memory allocations in creating new dictionaries to represent empty ones.
+ /// </summary>
+ /// <typeparam name="TKey">The type of the key.</typeparam>
+ /// <typeparam name="TValue">The type of the value.</typeparam>
+ [Serializable]
+ internal class EmptyDictionary<TKey, TValue> : IDictionary<TKey, TValue> {
+ /// <summary>
+ /// The singleton instance of the empty dictionary.
+ /// </summary>
+ internal static readonly EmptyDictionary<TKey, TValue> Instance = new EmptyDictionary<TKey, TValue>();
+
+ /// <summary>
+ /// Prevents a default instance of the EmptyDictionary class from being created.
+ /// </summary>
+ private EmptyDictionary() {
+ }
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </returns>
+ public ICollection<TValue> Values {
+ get { return EmptyList<TValue>.Instance; }
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </returns>
+ public int Count {
+ get { return 0; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </summary>
+ /// <value></value>
+ /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false.
+ /// </returns>
+ public bool IsReadOnly {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </returns>
+ public ICollection<TKey> Keys {
+ get { return EmptyList<TKey>.Instance; }
+ }
+
+ /// <summary>
+ /// Gets or sets the value with the specified key.
+ /// </summary>
+ /// <param name="key">The key being read or written.</param>
+ public TValue this[TKey key] {
+ get { throw new KeyNotFoundException(); }
+ set { throw new NotSupportedException(); }
+ }
+
+ /// <summary>
+ /// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </summary>
+ /// <param name="key">The object to use as the key of the element to add.</param>
+ /// <param name="value">The object to use as the value of the element to add.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="key"/> is null.
+ /// </exception>
+ /// <exception cref="T:System.ArgumentException">
+ /// An element with the same key already exists in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </exception>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.
+ /// </exception>
+ public void Add(TKey key, TValue value) {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key.
+ /// </summary>
+ /// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</param>
+ /// <returns>
+ /// true if the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the key; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="key"/> is null.
+ /// </exception>
+ public bool ContainsKey(TKey key) {
+ return false;
+ }
+
+ /// <summary>
+ /// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </summary>
+ /// <param name="key">The key of the element to remove.</param>
+ /// <returns>
+ /// true if the element is successfully removed; otherwise, false. This method also returns false if <paramref name="key"/> was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2"/>.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="key"/> is null.
+ /// </exception>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.
+ /// </exception>
+ public bool Remove(TKey key) {
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the value associated with the specified key.
+ /// </summary>
+ /// <param name="key">The key whose value to get.</param>
+ /// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value"/> parameter. This parameter is passed uninitialized.</param>
+ /// <returns>
+ /// true if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="key"/> is null.
+ /// </exception>
+ public bool TryGetValue(TKey key, out TValue value) {
+ value = default(TValue);
+ return false;
+ }
+
+ #region ICollection<KeyValuePair<TKey,TValue>> Members
+
+ /// <summary>
+ /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")]
+ public void Add(KeyValuePair<TKey, TValue> item) {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ public void Clear() {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"/> contains a specific value.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <returns>
+ /// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false.
+ /// </returns>
+ public bool Contains(KeyValuePair<TKey, TValue> item) {
+ return false;
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
+ /// </summary>
+ /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="array"/> is null.
+ /// </exception>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">
+ /// <paramref name="arrayIndex"/> is less than 0.
+ /// </exception>
+ /// <exception cref="T:System.ArgumentException">
+ /// <paramref name="array"/> is multidimensional.
+ /// -or-
+ /// <paramref name="arrayIndex"/> is equal to or greater than the length of <paramref name="array"/>.
+ /// -or-
+ /// The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.
+ /// -or-
+ /// Type cannot be cast automatically to the type of the destination <paramref name="array"/>.
+ /// </exception>
+ public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <returns>
+ /// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </returns>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ public bool Remove(KeyValuePair<TKey, TValue> item) {
+ return false;
+ }
+
+ #endregion
+
+ #region IEnumerable<KeyValuePair<TKey,TValue>> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
+ return Enumerable.Empty<KeyValuePair<TKey, TValue>>().GetEnumerator();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return EmptyEnumerator.Instance;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/EmptyEnumerator.cs b/src/DotNetOpenAuth.Core/Messaging/EmptyEnumerator.cs
new file mode 100644
index 0000000..f37e3d4
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/EmptyEnumerator.cs
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------
+// <copyright file="EmptyEnumerator.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System.Collections;
+
+ /// <summary>
+ /// An enumerator that always generates zero elements.
+ /// </summary>
+ internal class EmptyEnumerator : IEnumerator {
+ /// <summary>
+ /// The singleton instance of this empty enumerator.
+ /// </summary>
+ internal static readonly EmptyEnumerator Instance = new EmptyEnumerator();
+
+ /// <summary>
+ /// Prevents a default instance of the <see cref="EmptyEnumerator"/> class from being created.
+ /// </summary>
+ private EmptyEnumerator() {
+ }
+
+ #region IEnumerator Members
+
+ /// <summary>
+ /// Gets the current element in the collection.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// The current element in the collection.
+ /// </returns>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The enumerator is positioned before the first element of the collection or after the last element.
+ /// </exception>
+ public object Current {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Advances the enumerator to the next element of the collection.
+ /// </summary>
+ /// <returns>
+ /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.
+ /// </returns>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public bool MoveNext() {
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the enumerator to its initial position, which is before the first element in the collection.
+ /// </summary>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public void Reset() {
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/EmptyList.cs b/src/DotNetOpenAuth.Core/Messaging/EmptyList.cs
new file mode 100644
index 0000000..68cdabd
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/EmptyList.cs
@@ -0,0 +1,211 @@
+//-----------------------------------------------------------------------
+// <copyright file="EmptyList.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// An empty, read-only list.
+ /// </summary>
+ /// <typeparam name="T">The type the list claims to include.</typeparam>
+ [Serializable]
+ internal class EmptyList<T> : IList<T> {
+ /// <summary>
+ /// The singleton instance of the empty list.
+ /// </summary>
+ internal static readonly EmptyList<T> Instance = new EmptyList<T>();
+
+ /// <summary>
+ /// Prevents a default instance of the EmptyList class from being created.
+ /// </summary>
+ private EmptyList() {
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </returns>
+ public int Count {
+ get { return 0; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </summary>
+ /// <value></value>
+ /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false.
+ /// </returns>
+ public bool IsReadOnly {
+ get { return true; }
+ }
+
+ #region IList<T> Members
+
+ /// <summary>
+ /// Gets or sets the <typeparamref name="T"/> at the specified index.
+ /// </summary>
+ /// <param name="index">The index of the element in the list to change.</param>
+ public T this[int index] {
+ get {
+ throw new ArgumentOutOfRangeException("index");
+ }
+
+ set {
+ throw new ArgumentOutOfRangeException("index");
+ }
+ }
+
+ /// <summary>
+ /// Determines the index of a specific item in the <see cref="T:System.Collections.Generic.IList`1"/>.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
+ /// <returns>
+ /// The index of <paramref name="item"/> if found in the list; otherwise, -1.
+ /// </returns>
+ public int IndexOf(T item) {
+ return -1;
+ }
+
+ /// <summary>
+ /// Inserts an item to the <see cref="T:System.Collections.Generic.IList`1"/> at the specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
+ /// <param name="item">The object to insert into the <see cref="T:System.Collections.Generic.IList`1"/>.</param>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">
+ /// <paramref name="index"/> is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"/>.
+ /// </exception>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.IList`1"/> is read-only.
+ /// </exception>
+ public void Insert(int index, T item) {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Removes the <see cref="T:System.Collections.Generic.IList`1"/> item at the specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index of the item to remove.</param>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">
+ /// <paramref name="index"/> is not a valid index in the <see cref="T:System.Collections.Generic.IList`1"/>.
+ /// </exception>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.IList`1"/> is read-only.
+ /// </exception>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")]
+ public void RemoveAt(int index) {
+ throw new ArgumentOutOfRangeException("index");
+ }
+
+ #endregion
+
+ #region ICollection<T> Members
+
+ /// <summary>
+ /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")]
+ public void Add(T item) {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ public void Clear() {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"/> contains a specific value.
+ /// </summary>
+ /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <returns>
+ /// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false.
+ /// </returns>
+ public bool Contains(T item) {
+ return false;
+ }
+
+ /// <summary>
+ /// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
+ /// </summary>
+ /// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// <paramref name="array"/> is null.
+ /// </exception>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">
+ /// <paramref name="arrayIndex"/> is less than 0.
+ /// </exception>
+ /// <exception cref="T:System.ArgumentException">
+ /// <paramref name="array"/> is multidimensional.
+ /// -or-
+ /// <paramref name="arrayIndex"/> is equal to or greater than the length of <paramref name="array"/>.
+ /// -or-
+ /// The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.
+ /// -or-
+ /// Type <typeparamref name="T"/> cannot be cast automatically to the type of the destination <paramref name="array"/>.
+ /// </exception>
+ public void CopyTo(T[] array, int arrayIndex) {
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
+ /// <returns>
+ /// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </returns>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ public bool Remove(T item) {
+ return false;
+ }
+
+ #endregion
+
+ #region IEnumerable<T> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<T> GetEnumerator() {
+ return System.Linq.Enumerable.Empty<T>().GetEnumerator();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return EmptyEnumerator.Instance;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/EnumerableCache.cs b/src/DotNetOpenAuth.Core/Messaging/EnumerableCache.cs
new file mode 100644
index 0000000..f6ea55e
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/EnumerableCache.cs
@@ -0,0 +1,243 @@
+//-----------------------------------------------------------------------
+// <copyright file="EnumerableCache.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This code is released under the Microsoft Public License (Ms-PL).
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Extension methods for <see cref="IEnumerable&lt;T&gt;"/> types.
+ /// </summary>
+ public static class EnumerableCacheExtensions {
+ /// <summary>
+ /// Caches the results of enumerating over a given object so that subsequence enumerations
+ /// don't require interacting with the object a second time.
+ /// </summary>
+ /// <typeparam name="T">The type of element found in the enumeration.</typeparam>
+ /// <param name="sequence">The enumerable object.</param>
+ /// <returns>
+ /// Either a new enumerable object that caches enumerated results, or the original, <paramref name="sequence"/>
+ /// object if no caching is necessary to avoid additional CPU work.
+ /// </returns>
+ /// <remarks>
+ /// <para>This is designed for use on the results of generator methods (the ones with <c>yield return</c> in them)
+ /// so that only those elements in the sequence that are needed are ever generated, while not requiring
+ /// regeneration of elements that are enumerated over multiple times.</para>
+ /// <para>This can be a huge performance gain if enumerating multiple times over an expensive generator method.</para>
+ /// <para>Some enumerable types such as collections, lists, and already-cached generators do not require
+ /// any (additional) caching, and this method will simply return those objects rather than caching them
+ /// to avoid double-caching.</para>
+ /// </remarks>
+ public static IEnumerable<T> CacheGeneratedResults<T>(this IEnumerable<T> sequence) {
+ Requires.NotNull(sequence, "sequence");
+
+ // Don't create a cache for types that don't need it.
+ if (sequence is IList<T> ||
+ sequence is ICollection<T> ||
+ sequence is Array ||
+ sequence is EnumerableCache<T>) {
+ return sequence;
+ }
+
+ return new EnumerableCache<T>(sequence);
+ }
+
+ /// <summary>
+ /// A wrapper for <see cref="IEnumerable&lt;T&gt;"/> types and returns a caching <see cref="IEnumerator&lt;T&gt;"/>
+ /// from its <see cref="IEnumerable&lt;T&gt;.GetEnumerator"/> method.
+ /// </summary>
+ /// <typeparam name="T">The type of element in the sequence.</typeparam>
+ private class EnumerableCache<T> : IEnumerable<T> {
+ /// <summary>
+ /// The results from enumeration of the live object that have been collected thus far.
+ /// </summary>
+ private List<T> cache;
+
+ /// <summary>
+ /// The original generator method or other enumerable object whose contents should only be enumerated once.
+ /// </summary>
+ private IEnumerable<T> generator;
+
+ /// <summary>
+ /// The enumerator we're using over the generator method's results.
+ /// </summary>
+ private IEnumerator<T> generatorEnumerator;
+
+ /// <summary>
+ /// The sync object our caching enumerators use when adding a new live generator method result to the cache.
+ /// </summary>
+ /// <remarks>
+ /// Although individual enumerators are not thread-safe, this <see cref="IEnumerable&lt;T&gt;"/> should be
+ /// thread safe so that multiple enumerators can be created from it and used from different threads.
+ /// </remarks>
+ private object generatorLock = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the EnumerableCache class.
+ /// </summary>
+ /// <param name="generator">The generator.</param>
+ internal EnumerableCache(IEnumerable<T> generator) {
+ Requires.NotNull(generator, "generator");
+
+ this.generator = generator;
+ }
+
+ #region IEnumerable<T> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<T> GetEnumerator() {
+ if (this.generatorEnumerator == null) {
+ this.cache = new List<T>();
+ this.generatorEnumerator = this.generator.GetEnumerator();
+ }
+
+ return new EnumeratorCache(this);
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return this.GetEnumerator();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// An enumerator that uses cached enumeration results whenever they are available,
+ /// and caches whatever results it has to pull from the original <see cref="IEnumerable&lt;T&gt;"/> object.
+ /// </summary>
+ private class EnumeratorCache : IEnumerator<T> {
+ /// <summary>
+ /// The parent enumeration wrapper class that stores the cached results.
+ /// </summary>
+ private EnumerableCache<T> parent;
+
+ /// <summary>
+ /// The position of this enumerator in the cached list.
+ /// </summary>
+ private int cachePosition = -1;
+
+ /// <summary>
+ /// Initializes a new instance of the EnumeratorCache class.
+ /// </summary>
+ /// <param name="parent">The parent cached enumerable whose GetEnumerator method is calling this constructor.</param>
+ internal EnumeratorCache(EnumerableCache<T> parent) {
+ Requires.NotNull(parent, "parent");
+
+ this.parent = parent;
+ }
+
+ #region IEnumerator<T> Members
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ /// <returns>
+ /// The element in the collection at the current position of the enumerator.
+ /// </returns>
+ public T Current {
+ get {
+ if (this.cachePosition < 0 || this.cachePosition >= this.parent.cache.Count) {
+ throw new InvalidOperationException();
+ }
+
+ return this.parent.cache[this.cachePosition];
+ }
+ }
+
+ #endregion
+
+ #region IEnumerator Properties
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ /// <returns>
+ /// The element in the collection at the current position of the enumerator.
+ /// </returns>
+ object System.Collections.IEnumerator.Current {
+ get { return this.Current; }
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ #region IEnumerator Methods
+
+ /// <summary>
+ /// Advances the enumerator to the next element of the collection.
+ /// </summary>
+ /// <returns>
+ /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.
+ /// </returns>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public bool MoveNext() {
+ this.cachePosition++;
+ if (this.cachePosition >= this.parent.cache.Count) {
+ lock (this.parent.generatorLock) {
+ if (this.parent.generatorEnumerator.MoveNext()) {
+ this.parent.cache.Add(this.parent.generatorEnumerator.Current);
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Sets the enumerator to its initial position, which is before the first element in the collection.
+ /// </summary>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public void Reset() {
+ this.cachePosition = -1;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ // Nothing to do here.
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs
new file mode 100644
index 0000000..c6a652b
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs
@@ -0,0 +1,365 @@
+//-----------------------------------------------------------------------
+// <copyright file="ErrorUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Web;
+
+ /// <summary>
+ /// A collection of error checking and reporting methods.
+ /// </summary>
+ [ContractVerification(true)]
+ [Pure]
+ internal static class ErrorUtilities {
+ /// <summary>
+ /// Wraps an exception in a new <see cref="ProtocolException"/>.
+ /// </summary>
+ /// <param name="inner">The inner exception to wrap.</param>
+ /// <param name="errorMessage">The error message for the outer exception.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <returns>The newly constructed (unthrown) exception.</returns>
+ [Pure]
+ internal static Exception Wrap(Exception inner, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Assume(errorMessage != null);
+ return new ProtocolException(string.Format(CultureInfo.CurrentCulture, errorMessage, args), inner);
+ }
+
+ /// <summary>
+ /// Throws an internal error exception.
+ /// </summary>
+ /// <param name="errorMessage">The error message.</param>
+ /// <returns>Nothing. But included here so callers can "throw" this method for C# safety.</returns>
+ /// <exception cref="InternalErrorException">Always thrown.</exception>
+ [Pure]
+ internal static Exception ThrowInternal(string errorMessage) {
+ // Since internal errors are really bad, take this chance to
+ // help the developer find the cause by breaking into the
+ // debugger if one is attached.
+ if (Debugger.IsAttached) {
+ Debugger.Break();
+ }
+
+ throw new InternalErrorException(errorMessage);
+ }
+
+ /// <summary>
+ /// Checks a condition and throws an internal error exception if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <exception cref="InternalErrorException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyInternal(bool condition, string errorMessage) {
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<InternalErrorException>(!condition);
+ if (!condition) {
+ ThrowInternal(errorMessage);
+ }
+ }
+
+ /// <summary>
+ /// Checks a condition and throws an internal error exception if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <param name="args">The formatting arguments.</param>
+ /// <exception cref="InternalErrorException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyInternal(bool condition, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<InternalErrorException>(!condition);
+ Contract.Assume(errorMessage != null);
+ if (!condition) {
+ errorMessage = string.Format(CultureInfo.CurrentCulture, errorMessage, args);
+ throw new InternalErrorException(errorMessage);
+ }
+ }
+
+ /// <summary>
+ /// Checks a condition and throws an <see cref="InvalidOperationException"/> if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <exception cref="InvalidOperationException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyOperation(bool condition, string errorMessage) {
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<InvalidOperationException>(!condition);
+ if (!condition) {
+ throw new InvalidOperationException(errorMessage);
+ }
+ }
+
+ /// <summary>
+ /// Checks a condition and throws a <see cref="NotSupportedException"/> if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <exception cref="NotSupportedException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifySupported(bool condition, string errorMessage) {
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<NotSupportedException>(!condition);
+ if (!condition) {
+ throw new NotSupportedException(errorMessage);
+ }
+ }
+
+ /// <summary>
+ /// Checks a condition and throws a <see cref="NotSupportedException"/> if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <param name="args">The string formatting arguments for <paramref name="errorMessage"/>.</param>
+ /// <exception cref="NotSupportedException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifySupported(bool condition, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<NotSupportedException>(!condition);
+ Contract.Assume(errorMessage != null);
+ if (!condition) {
+ throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, errorMessage, args));
+ }
+ }
+
+ /// <summary>
+ /// Checks a condition and throws an <see cref="InvalidOperationException"/> if it evaluates to false.
+ /// </summary>
+ /// <param name="condition">The condition to check.</param>
+ /// <param name="errorMessage">The message to include in the exception, if created.</param>
+ /// <param name="args">The formatting arguments.</param>
+ /// <exception cref="InvalidOperationException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyOperation(bool condition, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<InvalidOperationException>(!condition);
+ Contract.Assume(errorMessage != null);
+ if (!condition) {
+ errorMessage = string.Format(CultureInfo.CurrentCulture, errorMessage, args);
+ throw new InvalidOperationException(errorMessage);
+ }
+ }
+
+ /// <summary>
+ /// Throws a <see cref="HostErrorException"/> if some <paramref name="condition"/> evaluates to false.
+ /// </summary>
+ /// <param name="condition">True to do nothing; false to throw the exception.</param>
+ /// <param name="errorMessage">The error message for the exception.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <exception cref="HostErrorException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyHost(bool condition, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<ProtocolException>(!condition);
+ Contract.Assume(errorMessage != null);
+ if (!condition) {
+ throw new HostErrorException(string.Format(CultureInfo.CurrentCulture, errorMessage, args));
+ }
+ }
+
+ /// <summary>
+ /// Throws a <see cref="ProtocolException"/> if some <paramref name="condition"/> evaluates to false.
+ /// </summary>
+ /// <param name="condition">True to do nothing; false to throw the exception.</param>
+ /// <param name="faultedMessage">The message being processed that would be responsible for the exception if thrown.</param>
+ /// <param name="errorMessage">The error message for the exception.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <exception cref="ProtocolException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyProtocol(bool condition, IProtocolMessage faultedMessage, string errorMessage, params object[] args) {
+ Requires.NotNull(args, "args");
+ Requires.NotNull(faultedMessage, "faultedMessage");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<ProtocolException>(!condition);
+ Contract.Assume(errorMessage != null);
+ if (!condition) {
+ throw new ProtocolException(string.Format(CultureInfo.CurrentCulture, errorMessage, args), faultedMessage);
+ }
+ }
+
+ /// <summary>
+ /// Throws a <see cref="ProtocolException"/> if some <paramref name="condition"/> evaluates to false.
+ /// </summary>
+ /// <param name="condition">True to do nothing; false to throw the exception.</param>
+ /// <param name="message">The error message for the exception.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <exception cref="ProtocolException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyProtocol(bool condition, string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<ProtocolException>(!condition);
+ Contract.Assume(message != null);
+ if (!condition) {
+ var exception = new ProtocolException(string.Format(CultureInfo.CurrentCulture, message, args));
+ if (Logger.Messaging.IsErrorEnabled) {
+ Logger.Messaging.Error(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ "Protocol error: {0}{1}{2}",
+ exception.Message,
+ Environment.NewLine,
+ new StackTrace()));
+ }
+ throw exception;
+ }
+ }
+
+ /// <summary>
+ /// Throws a <see cref="ProtocolException"/>.
+ /// </summary>
+ /// <param name="message">The message to set in the exception.</param>
+ /// <param name="args">The formatting arguments of the message.</param>
+ /// <returns>
+ /// An InternalErrorException, which may be "thrown" by the caller in order
+ /// to satisfy C# rules to show that code will never be reached, but no value
+ /// actually is ever returned because this method guarantees to throw.
+ /// </returns>
+ /// <exception cref="ProtocolException">Always thrown.</exception>
+ [Pure]
+ internal static Exception ThrowProtocol(string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Assume(message != null);
+ VerifyProtocol(false, message, args);
+
+ // we never reach here, but this allows callers to "throw" this method.
+ return new InternalErrorException();
+ }
+
+ /// <summary>
+ /// Throws a <see cref="FormatException"/>.
+ /// </summary>
+ /// <param name="message">The message for the exception.</param>
+ /// <param name="args">The string formatting arguments for <paramref name="message"/>.</param>
+ /// <returns>Nothing. It's just here so the caller can throw this method for C# compilation check.</returns>
+ [Pure]
+ internal static Exception ThrowFormat(string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Assume(message != null);
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, message, args));
+ }
+
+ /// <summary>
+ /// Throws a <see cref="FormatException"/> if some condition is false.
+ /// </summary>
+ /// <param name="condition">The expression to evaluate. A value of <c>false</c> will cause the exception to be thrown.</param>
+ /// <param name="message">The message for the exception.</param>
+ /// <param name="args">The string formatting arguments for <paramref name="message"/>.</param>
+ /// <exception cref="FormatException">Thrown when <paramref name="condition"/> is <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyFormat(bool condition, string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<FormatException>(!condition);
+ Contract.Assume(message != null);
+ if (!condition) {
+ throw ThrowFormat(message, args);
+ }
+ }
+
+ /// <summary>
+ /// Verifies something about the argument supplied to a method.
+ /// </summary>
+ /// <param name="condition">The condition that must evaluate to true to avoid an exception.</param>
+ /// <param name="message">The message to use in the exception if the condition is false.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyArgument(bool condition, string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<ArgumentException>(!condition);
+ Contract.Assume(message != null);
+ if (!condition) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, message, args));
+ }
+ }
+
+ /// <summary>
+ /// Throws an <see cref="ArgumentException"/>.
+ /// </summary>
+ /// <param name="parameterName">Name of the parameter.</param>
+ /// <param name="message">The message to use in the exception if the condition is false.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <returns>Never returns anything. It always throws.</returns>
+ [Pure]
+ internal static Exception ThrowArgumentNamed(string parameterName, string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Assume(message != null);
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, message, args), parameterName);
+ }
+
+ /// <summary>
+ /// Verifies something about the argument supplied to a method.
+ /// </summary>
+ /// <param name="condition">The condition that must evaluate to true to avoid an exception.</param>
+ /// <param name="parameterName">Name of the parameter.</param>
+ /// <param name="message">The message to use in the exception if the condition is false.</param>
+ /// <param name="args">The string formatting arguments, if any.</param>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception>
+ [Pure]
+ internal static void VerifyArgumentNamed(bool condition, string parameterName, string message, params object[] args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(condition);
+ Contract.EnsuresOnThrow<ArgumentException>(!condition);
+ Contract.Assume(message != null);
+ if (!condition) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, message, args), parameterName);
+ }
+ }
+
+ /// <summary>
+ /// Verifies that some given value is not null.
+ /// </summary>
+ /// <param name="value">The value to check.</param>
+ /// <param name="paramName">Name of the parameter, which will be used in the <see cref="ArgumentException"/>, if thrown.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
+ [Pure]
+ internal static void VerifyArgumentNotNull(object value, string paramName) {
+ Contract.Ensures(value != null);
+ Contract.EnsuresOnThrow<ArgumentNullException>(value == null);
+ if (value == null) {
+ throw new ArgumentNullException(paramName);
+ }
+ }
+
+ /// <summary>
+ /// Verifies that some string is not null and has non-zero length.
+ /// </summary>
+ /// <param name="value">The value to check.</param>
+ /// <param name="paramName">Name of the parameter, which will be used in the <see cref="ArgumentException"/>, if thrown.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="value"/> has zero length.</exception>
+ [Pure]
+ internal static void VerifyNonZeroLength(string value, string paramName) {
+ Contract.Ensures((value != null && value.Length > 0) && !string.IsNullOrEmpty(value));
+ Contract.EnsuresOnThrow<ArgumentException>(value == null || value.Length == 0);
+ VerifyArgumentNotNull(value, paramName);
+ if (value.Length == 0) {
+ throw new ArgumentException(MessagingStrings.UnexpectedEmptyString, paramName);
+ }
+ }
+
+ /// <summary>
+ /// Verifies that <see cref="HttpContext.Current"/> != <c>null</c>.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current"/> == <c>null</c></exception>
+ [Pure]
+ internal static void VerifyHttpContext() {
+ Contract.Ensures(HttpContext.Current != null);
+ Contract.Ensures(HttpContext.Current.Request != null);
+ ErrorUtilities.VerifyOperation(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Exceptions.cd b/src/DotNetOpenAuth.Core/Messaging/Exceptions.cd
new file mode 100644
index 0000000..0119753
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Exceptions.cd
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="DotNetOpenAuth.Messaging.ProtocolException" Collapsed="true">
+ <Position X="3.25" Y="0.75" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>ICAMAAAAQAAAgAEAAIBAAAYgCgAAIAAAIACAACAAAAA=</HashCode>
+ <FileName>Messaging\ProtocolException.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.InvalidSignatureException" Collapsed="true">
+ <Position X="3" Y="2.25" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\InvalidSignatureException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.ReplayedMessageException" Collapsed="true">
+ <Position X="5.25" Y="2.25" Width="2.25" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\ReplayedMessageException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.Messaging.Bindings.ExpiredMessageException">
+ <Position X="0.75" Y="2.25" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\Bindings\ExpiredMessageException.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/HostErrorException.cs b/src/DotNetOpenAuth.Core/Messaging/HostErrorException.cs
new file mode 100644
index 0000000..81691b0
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/HostErrorException.cs
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------
+// <copyright file="HostErrorException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// An exception to call out a configuration or runtime failure on the part of the
+ /// (web) application that is hosting this library.
+ /// </summary>
+ /// <remarks>
+ /// <para>This exception is used rather than <see cref="ProtocolException"/> for those errors
+ /// that should never be caught because they indicate a major error in the app itself
+ /// or its configuration.</para>
+ /// <para>It is an internal exception to assist in making it uncatchable.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic", Justification = "We don't want this exception to be catchable.")]
+ [Serializable]
+ internal class HostErrorException : Exception {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostErrorException"/> class.
+ /// </summary>
+ internal HostErrorException() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostErrorException"/> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ internal HostErrorException(string message)
+ : base(message) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostErrorException"/> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="inner">The inner exception.</param>
+ internal HostErrorException(string message, Exception inner)
+ : base(message, inner) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostErrorException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The <paramref name="info"/> parameter is null.
+ /// </exception>
+ /// <exception cref="T:System.Runtime.Serialization.SerializationException">
+ /// The class name is null or <see cref="P:System.Exception.HResult"/> is zero (0).
+ /// </exception>
+ protected HostErrorException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs b/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs
new file mode 100644
index 0000000..1443fff
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs
@@ -0,0 +1,58 @@
+//-----------------------------------------------------------------------
+// <copyright file="HttpDeliveryMethods.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// The methods available for the local party to send messages to a remote party.
+ /// </summary>
+ /// <remarks>
+ /// See OAuth 1.0 spec section 5.2.
+ /// </remarks>
+ [Flags]
+ public enum HttpDeliveryMethods {
+ /// <summary>
+ /// No HTTP methods are allowed.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// In the HTTP Authorization header as defined in OAuth HTTP Authorization Scheme (OAuth HTTP Authorization Scheme).
+ /// </summary>
+ AuthorizationHeaderRequest = 0x1,
+
+ /// <summary>
+ /// As the HTTP POST request body with a content-type of application/x-www-form-urlencoded.
+ /// </summary>
+ PostRequest = 0x2,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary>
+ GetRequest = 0x4,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary>
+ PutRequest = 0x8,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary>
+ DeleteRequest = 0x10,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary>
+ HeadRequest = 0x20,
+
+ /// <summary>
+ /// The flags that control HTTP verbs.
+ /// </summary>
+ HttpVerbMask = PostRequest | GetRequest | PutRequest | DeleteRequest | HeadRequest,
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs
new file mode 100644
index 0000000..0cf37a5
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs
@@ -0,0 +1,423 @@
+//-----------------------------------------------------------------------
+// <copyright file="HttpRequestInfo.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Specialized;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Net;
+ using System.Net.Mime;
+ using System.ServiceModel.Channels;
+ using System.Web;
+
+ /// <summary>
+ /// A property store of details of an incoming HTTP request.
+ /// </summary>
+ /// <remarks>
+ /// This serves a very similar purpose to <see cref="HttpRequest"/>, except that
+ /// ASP.NET does not let us fully initialize that class, so we have to write one
+ /// of our one.
+ /// </remarks>
+ public class HttpRequestInfo {
+ /// <summary>
+ /// The key/value pairs found in the entity of a POST request.
+ /// </summary>
+ private NameValueCollection form;
+
+ /// <summary>
+ /// The key/value pairs found in the querystring of the incoming request.
+ /// </summary>
+ private NameValueCollection queryString;
+
+ /// <summary>
+ /// Backing field for the <see cref="QueryStringBeforeRewriting"/> property.
+ /// </summary>
+ private NameValueCollection queryStringBeforeRewriting;
+
+ /// <summary>
+ /// Backing field for the <see cref="Message"/> property.
+ /// </summary>
+ private IDirectedProtocolMessage message;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="request">The ASP.NET structure to copy from.</param>
+ public HttpRequestInfo(HttpRequest request) {
+ Requires.NotNull(request, "request");
+ Contract.Ensures(this.HttpMethod == request.HttpMethod);
+ Contract.Ensures(this.Url == request.Url);
+ Contract.Ensures(this.RawUrl == request.RawUrl);
+ Contract.Ensures(this.UrlBeforeRewriting != null);
+ Contract.Ensures(this.Headers != null);
+ Contract.Ensures(this.InputStream == request.InputStream);
+ Contract.Ensures(this.form == request.Form);
+ Contract.Ensures(this.queryString == request.QueryString);
+
+ this.HttpMethod = request.HttpMethod;
+ this.Url = request.Url;
+ this.UrlBeforeRewriting = GetPublicFacingUrl(request);
+ this.RawUrl = request.RawUrl;
+ this.Headers = GetHeaderCollection(request.Headers);
+ this.InputStream = request.InputStream;
+
+ // These values would normally be calculated, but we'll reuse them from
+ // HttpRequest since they're already calculated, and there's a chance (<g>)
+ // that ASP.NET does a better job of being comprehensive about gathering
+ // these as well.
+ this.form = request.Form;
+ this.queryString = request.QueryString;
+
+ Reporting.RecordRequestStatistics(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="httpMethod">The HTTP method (i.e. GET or POST) of the incoming request.</param>
+ /// <param name="requestUrl">The URL being requested.</param>
+ /// <param name="rawUrl">The raw URL that appears immediately following the HTTP verb in the request,
+ /// before any URL rewriting takes place.</param>
+ /// <param name="headers">Headers in the HTTP request.</param>
+ /// <param name="inputStream">The entity stream, if any. (POST requests typically have these). Use <c>null</c> for GET requests.</param>
+ public HttpRequestInfo(string httpMethod, Uri requestUrl, string rawUrl, WebHeaderCollection headers, Stream inputStream) {
+ Requires.NotNullOrEmpty(httpMethod, "httpMethod");
+ Requires.NotNull(requestUrl, "requestUrl");
+ Requires.NotNull(rawUrl, "rawUrl");
+ Requires.NotNull(headers, "headers");
+
+ this.HttpMethod = httpMethod;
+ this.Url = requestUrl;
+ this.UrlBeforeRewriting = requestUrl;
+ this.RawUrl = rawUrl;
+ this.Headers = headers;
+ this.InputStream = inputStream;
+
+ Reporting.RecordRequestStatistics(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="listenerRequest">Details on the incoming HTTP request.</param>
+ public HttpRequestInfo(HttpListenerRequest listenerRequest) {
+ Requires.NotNull(listenerRequest, "listenerRequest");
+
+ this.HttpMethod = listenerRequest.HttpMethod;
+ this.Url = listenerRequest.Url;
+ this.UrlBeforeRewriting = listenerRequest.Url;
+ this.RawUrl = listenerRequest.RawUrl;
+ this.Headers = new WebHeaderCollection();
+ foreach (string key in listenerRequest.Headers) {
+ this.Headers[key] = listenerRequest.Headers[key];
+ }
+
+ this.InputStream = listenerRequest.InputStream;
+
+ Reporting.RecordRequestStatistics(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="request">The WCF incoming request structure to get the HTTP information from.</param>
+ /// <param name="requestUri">The URI of the service endpoint.</param>
+ public HttpRequestInfo(HttpRequestMessageProperty request, Uri requestUri) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(requestUri, "requestUri");
+
+ this.HttpMethod = request.Method;
+ this.Headers = request.Headers;
+ this.Url = requestUri;
+ this.UrlBeforeRewriting = requestUri;
+ this.RawUrl = MakeUpRawUrlFromUrl(requestUri);
+
+ Reporting.RecordRequestStatistics(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ internal HttpRequestInfo() {
+ Contract.Ensures(this.HttpMethod == "GET");
+ Contract.Ensures(this.Headers != null);
+
+ this.HttpMethod = "GET";
+ this.Headers = new WebHeaderCollection();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="request">The HttpWebRequest (that was never used) to copy from.</param>
+ internal HttpRequestInfo(WebRequest request) {
+ Requires.NotNull(request, "request");
+
+ this.HttpMethod = request.Method;
+ this.Url = request.RequestUri;
+ this.UrlBeforeRewriting = request.RequestUri;
+ this.RawUrl = MakeUpRawUrlFromUrl(request.RequestUri);
+ this.Headers = GetHeaderCollection(request.Headers);
+ this.InputStream = null;
+
+ Reporting.RecordRequestStatistics(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
+ /// </summary>
+ /// <param name="message">The message being passed in through a mock transport. May be null.</param>
+ /// <param name="httpMethod">The HTTP method that the incoming request came in on, whether or not <paramref name="message"/> is null.</param>
+ internal HttpRequestInfo(IDirectedProtocolMessage message, HttpDeliveryMethods httpMethod) {
+ this.message = message;
+ this.HttpMethod = MessagingUtilities.GetHttpVerb(httpMethod);
+ }
+
+ /// <summary>
+ /// Gets or sets the message that is being sent over a mock transport (for testing).
+ /// </summary>
+ internal virtual IDirectedProtocolMessage Message {
+ get { return this.message; }
+ set { this.message = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the verb in the request (i.e. GET, POST, etc.)
+ /// </summary>
+ internal string HttpMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the entire URL of the request, after any URL rewriting.
+ /// </summary>
+ internal Uri Url { get; set; }
+
+ /// <summary>
+ /// Gets or sets the raw URL that appears immediately following the HTTP verb in the request,
+ /// before any URL rewriting takes place.
+ /// </summary>
+ internal string RawUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the full public URL used by the remote client to initiate this request,
+ /// before any URL rewriting and before any changes made by web farm load distributors.
+ /// </summary>
+ internal Uri UrlBeforeRewriting { get; set; }
+
+ /// <summary>
+ /// Gets the query part of the URL (The ? and everything after it), after URL rewriting.
+ /// </summary>
+ internal string Query {
+ get { return this.Url != null ? this.Url.Query : null; }
+ }
+
+ /// <summary>
+ /// Gets or sets the collection of headers that came in with the request.
+ /// </summary>
+ internal WebHeaderCollection Headers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the entity, or body of the request, if any.
+ /// </summary>
+ internal Stream InputStream { get; set; }
+
+ /// <summary>
+ /// Gets the key/value pairs found in the entity of a POST request.
+ /// </summary>
+ internal NameValueCollection Form {
+ get {
+ Contract.Ensures(Contract.Result<NameValueCollection>() != null);
+ if (this.form == null) {
+ ContentType contentType = string.IsNullOrEmpty(this.Headers[HttpRequestHeader.ContentType]) ? null : new ContentType(this.Headers[HttpRequestHeader.ContentType]);
+ if (this.HttpMethod == "POST" && contentType != null && string.Equals(contentType.MediaType, Channel.HttpFormUrlEncoded, StringComparison.Ordinal)) {
+ StreamReader reader = new StreamReader(this.InputStream);
+ long originalPosition = 0;
+ if (this.InputStream.CanSeek) {
+ originalPosition = this.InputStream.Position;
+ }
+ this.form = HttpUtility.ParseQueryString(reader.ReadToEnd());
+ if (this.InputStream.CanSeek) {
+ this.InputStream.Seek(originalPosition, SeekOrigin.Begin);
+ }
+ } else {
+ this.form = new NameValueCollection();
+ }
+ }
+
+ return this.form;
+ }
+ }
+
+ /// <summary>
+ /// Gets the key/value pairs found in the querystring of the incoming request.
+ /// </summary>
+ internal NameValueCollection QueryString {
+ get {
+ if (this.queryString == null) {
+ this.queryString = this.Query != null ? HttpUtility.ParseQueryString(this.Query) : new NameValueCollection();
+ }
+
+ return this.queryString;
+ }
+ }
+
+ /// <summary>
+ /// Gets the query data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ /// <returns>A <see cref="NameValueCollection"/> containing all the parameters in the query string.</returns>
+ internal NameValueCollection QueryStringBeforeRewriting {
+ get {
+ if (this.queryStringBeforeRewriting == null) {
+ // This request URL may have been rewritten by the host site.
+ // For openid protocol purposes, we really need to look at
+ // the original query parameters before any rewriting took place.
+ if (!this.IsUrlRewritten) {
+ // No rewriting has taken place.
+ this.queryStringBeforeRewriting = this.QueryString;
+ } else {
+ // Rewriting detected! Recover the original request URI.
+ ErrorUtilities.VerifyInternal(this.UrlBeforeRewriting != null, "UrlBeforeRewriting is null, so the query string cannot be determined.");
+ this.queryStringBeforeRewriting = HttpUtility.ParseQueryString(this.UrlBeforeRewriting.Query);
+ }
+ }
+
+ return this.queryStringBeforeRewriting;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the request's URL was rewritten by ASP.NET
+ /// or some other module.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this request's URL was rewritten; otherwise, <c>false</c>.
+ /// </value>
+ internal bool IsUrlRewritten {
+ get { return this.Url != this.UrlBeforeRewriting; }
+ }
+
+ /// <summary>
+ /// Gets the public facing URL for the given incoming HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="serverVariables">The server variables to consider part of the request.</param>
+ /// <returns>
+ /// The URI that the outside world used to create this request.
+ /// </returns>
+ /// <remarks>
+ /// Although the <paramref name="serverVariables"/> value can be obtained from
+ /// <see cref="HttpRequest.ServerVariables"/>, it's useful to be able to pass them
+ /// in so we can simulate injected values from our unit tests since the actual property
+ /// is a read-only kind of <see cref="NameValueCollection"/>.
+ /// </remarks>
+ internal static Uri GetPublicFacingUrl(HttpRequest request, NameValueCollection serverVariables) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(serverVariables, "serverVariables");
+
+ // Due to URL rewriting, cloud computing (i.e. Azure)
+ // and web farms, etc., we have to be VERY careful about what
+ // we consider the incoming URL. We want to see the URL as it would
+ // appear on the public-facing side of the hosting web site.
+ // HttpRequest.Url gives us the internal URL in a cloud environment,
+ // So we use a variable that (at least from what I can tell) gives us
+ // the public URL:
+ if (serverVariables["HTTP_HOST"] != null) {
+ ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols.");
+ string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme;
+ Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]);
+ UriBuilder publicRequestUri = new UriBuilder(request.Url);
+ publicRequestUri.Scheme = scheme;
+ publicRequestUri.Host = hostAndPort.Host;
+ publicRequestUri.Port = hostAndPort.Port; // CC missing Uri.Port contract that's on UriBuilder.Port
+ return publicRequestUri.Uri;
+ } else {
+ // Failover to the method that works for non-web farm enviroments.
+ // We use Request.Url for the full path to the server, and modify it
+ // with Request.RawUrl to capture both the cookieless session "directory" if it exists
+ // and the original path in case URL rewriting is going on. We don't want to be
+ // fooled by URL rewriting because we're comparing the actual URL with what's in
+ // the return_to parameter in some cases.
+ // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless
+ // session, but not the URL rewriting problem.
+ return new Uri(request.Url, request.RawUrl);
+ }
+ }
+
+ /// <summary>
+ /// Gets the query or form data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ /// <returns>A set of name=value pairs.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Expensive call")]
+ internal NameValueCollection GetQueryOrFormFromContext() {
+ NameValueCollection query;
+ if (this.HttpMethod == "GET") {
+ query = this.QueryStringBeforeRewriting;
+ } else {
+ query = this.Form;
+ }
+ return query;
+ }
+
+ /// <summary>
+ /// Gets the public facing URL for the given incoming HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>The URI that the outside world used to create this request.</returns>
+ private static Uri GetPublicFacingUrl(HttpRequest request) {
+ Requires.NotNull(request, "request");
+ return GetPublicFacingUrl(request, request.ServerVariables);
+ }
+
+ /// <summary>
+ /// Makes up a reasonable guess at the raw URL from the possibly rewritten URL.
+ /// </summary>
+ /// <param name="url">A full URL.</param>
+ /// <returns>A raw URL that might have come in on the HTTP verb.</returns>
+ private static string MakeUpRawUrlFromUrl(Uri url) {
+ Requires.NotNull(url, "url");
+ return url.AbsolutePath + url.Query + url.Fragment;
+ }
+
+ /// <summary>
+ /// Converts a NameValueCollection to a WebHeaderCollection.
+ /// </summary>
+ /// <param name="pairs">The collection a HTTP headers.</param>
+ /// <returns>A new collection of the given headers.</returns>
+ private static WebHeaderCollection GetHeaderCollection(NameValueCollection pairs) {
+ Requires.NotNull(pairs, "pairs");
+
+ WebHeaderCollection headers = new WebHeaderCollection();
+ foreach (string key in pairs) {
+ try {
+ headers.Add(key, pairs[key]);
+ } catch (ArgumentException ex) {
+ Logger.Messaging.WarnFormat(
+ "{0} thrown when trying to add web header \"{1}: {2}\". {3}",
+ ex.GetType().Name,
+ key,
+ pairs[key],
+ ex.Message);
+ }
+ }
+
+ return headers;
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs b/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs
new file mode 100644
index 0000000..9dac9b3
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IChannelBindingElement.cs
@@ -0,0 +1,146 @@
+//-----------------------------------------------------------------------
+// <copyright file="IChannelBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// An interface that must be implemented by message transforms/validators in order
+ /// to be included in the channel stack.
+ /// </summary>
+ [ContractClass(typeof(IChannelBindingElementContract))]
+ public interface IChannelBindingElement {
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the protection commonly offered (if any) by this binding element.
+ /// </summary>
+ /// <remarks>
+ /// This value is used to assist in sorting binding elements in the channel stack.
+ /// </remarks>
+ MessageProtections Protection { get; }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ MessageProtections? ProcessOutgoingMessage(IProtocolMessage message);
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ MessageProtections? ProcessIncomingMessage(IProtocolMessage message);
+ }
+
+ /// <summary>
+ /// Code Contract for the <see cref="IChannelBindingElement"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IChannelBindingElement))]
+ internal abstract class IChannelBindingElementContract : IChannelBindingElement {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IChannelBindingElementContract"/> class from being created.
+ /// </summary>
+ private IChannelBindingElementContract() {
+ }
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ Channel IChannelBindingElement.Channel {
+ get { throw new NotImplementedException(); }
+ set { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the protection commonly offered (if any) by this binding element.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This value is used to assist in sorting binding elements in the channel stack.
+ /// </remarks>
+ MessageProtections IChannelBindingElement.Protection {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ MessageProtections? IChannelBindingElement.ProcessOutgoingMessage(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ Requires.ValidState(((IChannelBindingElement)this).Channel != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ MessageProtections? IChannelBindingElement.ProcessIncomingMessage(IProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ Requires.ValidState(((IChannelBindingElement)this).Channel != null);
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs
new file mode 100644
index 0000000..fd1c15d
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs
@@ -0,0 +1,75 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDataBagFormatter.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A serializer for <see cref="DataBag"/>-derived types
+ /// </summary>
+ /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam>
+ [ContractClass(typeof(IDataBagFormatterContract<>))]
+ internal interface IDataBagFormatter<T> where T : DataBag, new() {
+ /// <summary>
+ /// Serializes the specified message.
+ /// </summary>
+ /// <param name="message">The message to serialize. Must not be null.</param>
+ /// <returns>A non-null, non-empty value.</returns>
+ string Serialize(T message);
+
+ /// <summary>
+ /// Deserializes a <see cref="DataBag"/>.
+ /// </summary>
+ /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param>
+ /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param>
+ /// <returns>The deserialized value. Never null.</returns>
+ T Deserialize(IProtocolMessage containingMessage, string data);
+ }
+
+ /// <summary>
+ /// Contract class for the IDataBagFormatter interface.
+ /// </summary>
+ /// <typeparam name="T">The type of DataBag to serialize.</typeparam>
+ [ContractClassFor(typeof(IDataBagFormatter<>))]
+ internal abstract class IDataBagFormatterContract<T> : IDataBagFormatter<T> where T : DataBag, new() {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IDataBagFormatterContract&lt;T&gt;"/> class from being created.
+ /// </summary>
+ private IDataBagFormatterContract() {
+ }
+
+ #region IDataBagFormatter<T> Members
+
+ /// <summary>
+ /// Serializes the specified message.
+ /// </summary>
+ /// <param name="message">The message to serialize. Must not be null.</param>
+ /// <returns>A non-null, non-empty value.</returns>
+ string IDataBagFormatter<T>.Serialize(T message) {
+ Requires.NotNull(message, "message");
+ Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));
+
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Deserializes a <see cref="DataBag"/>.
+ /// </summary>
+ /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param>
+ /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param>
+ /// <returns>The deserialized value. Never null.</returns>
+ T IDataBagFormatter<T>.Deserialize(IProtocolMessage containingMessage, string data) {
+ Requires.NotNull(containingMessage, "containingMessage");
+ Requires.NotNullOrEmpty(data, "data");
+ Contract.Ensures(Contract.Result<T>() != null);
+
+ throw new System.NotImplementedException();
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/IDirectResponseProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/IDirectResponseProtocolMessage.cs
new file mode 100644
index 0000000..3b4da6c
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IDirectResponseProtocolMessage.cs
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectResponseProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ /// <summary>
+ /// Undirected messages that serve as direct responses to direct requests.
+ /// </summary>
+ public interface IDirectResponseProtocolMessage : IProtocolMessage {
+ /// <summary>
+ /// Gets the originating request message that caused this response to be formed.
+ /// </summary>
+ IDirectedProtocolMessage OriginatingRequest { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs
new file mode 100644
index 0000000..add35f9
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs
@@ -0,0 +1,223 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A contract for <see cref="HttpWebRequest"/> handling.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface must be thread safe.
+ /// </remarks>
+ [ContractClass(typeof(IDirectWebRequestHandlerContract))]
+ public interface IDirectWebRequestHandler {
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ bool CanSupport(DirectWebRequestOptions options);
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream GetRequestStream(HttpWebRequest request);
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options);
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ IncomingWebResponse GetResponse(HttpWebRequest request);
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IDirectWebRequestHandler"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IDirectWebRequestHandler))]
+ internal abstract class IDirectWebRequestHandlerContract : IDirectWebRequestHandler {
+ #region IDirectWebRequestHandler Members
+
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ bool IDirectWebRequestHandler.CanSupport(DirectWebRequestOptions options) {
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request) {
+ Requires.NotNull(request, "request");
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
+ Requires.NotNull(request, "request");
+ Requires.Support(((IDirectWebRequestHandler)this).CanSupport(options), MessagingStrings.DirectWebRequestOptionsNotSupported);
+ ////ErrorUtilities.VerifySupported(((IDirectWebRequestHandler)this).CanSupport(options), string.Format(MessagingStrings.DirectWebRequestOptionsNotSupported, options, this.GetType().Name));
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.
+ /// </remarks>
+ IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) {
+ Requires.NotNull(request, "request");
+ Contract.Ensures(Contract.Result<IncomingWebResponse>() != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null);
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.
+ /// </remarks>
+ IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
+ Requires.NotNull(request, "request");
+ Contract.Ensures(Contract.Result<IncomingWebResponse>() != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null);
+ Requires.Support(((IDirectWebRequestHandler)this).CanSupport(options), MessagingStrings.DirectWebRequestOptionsNotSupported);
+
+ ////ErrorUtilities.VerifySupported(((IDirectWebRequestHandler)this).CanSupport(options), string.Format(MessagingStrings.DirectWebRequestOptionsNotSupported, options, this.GetType().Name));
+ throw new System.NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs.orig b/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs.orig
new file mode 100644
index 0000000..a17b379
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IDirectWebRequestHandler.cs.orig
@@ -0,0 +1,222 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Net;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A contract for <see cref="HttpWebRequest"/> handling.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface must be thread safe.
+ /// </remarks>
+ public interface IDirectWebRequestHandler {
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ bool CanSupport(DirectWebRequestOptions options);
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream GetRequestStream(HttpWebRequest request);
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options);
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ IncomingWebResponse GetResponse(HttpWebRequest request);
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options);
+ }
+<<<<<<< HEAD
+=======
+
+ /// <summary>
+ /// Code contract for the <see cref="IDirectWebRequestHandler"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IDirectWebRequestHandler))]
+ internal abstract class IDirectWebRequestHandlerContract : IDirectWebRequestHandler {
+ #region IDirectWebRequestHandler Members
+
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ bool IDirectWebRequestHandler.CanSupport(DirectWebRequestOptions options) {
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The stream the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.
+ /// Callers <i>must</i> close and dispose of the request stream when they are done
+ /// writing to it to avoid taking up the connection too long and causing long waits on
+ /// subsequent requests.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Requires<NotSupportedException>(((IDirectWebRequestHandler)this).CanSupport(options), MessagingStrings.DirectWebRequestOptionsNotSupported);
+ ////ErrorUtilities.VerifySupported(((IDirectWebRequestHandler)this).CanSupport(options), string.Format(MessagingStrings.DirectWebRequestOptionsNotSupported, options, this.GetType().Name));
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.
+ /// </remarks>
+ IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>() != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null);
+ throw new System.NotImplementedException();
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.
+ /// </remarks>
+ IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Requires<NotSupportedException>(((IDirectWebRequestHandler)this).CanSupport(options), MessagingStrings.DirectWebRequestOptionsNotSupported);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>() != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null);
+
+ ////ErrorUtilities.VerifySupported(((IDirectWebRequestHandler)this).CanSupport(options), string.Format(MessagingStrings.DirectWebRequestOptionsNotSupported, options, this.GetType().Name));
+ throw new System.NotImplementedException();
+ }
+
+ #endregion
+ }
+>>>>>>> 884bcec... Fixed typo in comments.
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IDirectedProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/IDirectedProtocolMessage.cs
new file mode 100644
index 0000000..4342d45
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IDirectedProtocolMessage.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+// <copyright file="IDirectedProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// Implemented by messages that have explicit recipients
+ /// (direct requests and all indirect messages).
+ /// </summary>
+ public interface IDirectedProtocolMessage : IProtocolMessage {
+ /// <summary>
+ /// Gets the preferred method of transport for the message.
+ /// </summary>
+ /// <remarks>
+ /// For indirect messages this will likely be GET+POST, which both can be simulated in the user agent:
+ /// the GET with a simple 301 Redirect, and the POST with an HTML form in the response with javascript
+ /// to automate submission.
+ /// </remarks>
+ HttpDeliveryMethods HttpMethods { get; }
+
+ /// <summary>
+ /// Gets the URL of the intended receiver of this message.
+ /// </summary>
+ Uri Recipient { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IExtensionMessage.cs b/src/DotNetOpenAuth.Core/Messaging/IExtensionMessage.cs
new file mode 100644
index 0000000..5fc05a6
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IExtensionMessage.cs
@@ -0,0 +1,16 @@
+//-----------------------------------------------------------------------
+// <copyright file="IExtensionMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// An interface that extension messages must implement.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Justification = "Extension messages may gain members later on.")]
+ public interface IExtensionMessage : IMessage {
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponse.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponse.cs
new file mode 100644
index 0000000..20c3d6f
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponse.cs
@@ -0,0 +1,28 @@
+//-----------------------------------------------------------------------
+// <copyright file="IHttpDirectResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System.Diagnostics.Contracts;
+ using System.Net;
+
+ /// <summary>
+ /// An interface that allows direct response messages to specify
+ /// HTTP transport specific properties.
+ /// </summary>
+ [ContractClass(typeof(IHttpDirectResponseContract))]
+ public interface IHttpDirectResponse {
+ /// <summary>
+ /// Gets the HTTP status code that the direct response should be sent with.
+ /// </summary>
+ HttpStatusCode HttpStatusCode { get; }
+
+ /// <summary>
+ /// Gets the HTTP headers to add to the response.
+ /// </summary>
+ /// <value>May be an empty collection, but must not be <c>null</c>.</value>
+ WebHeaderCollection Headers { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponseContract.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponseContract.cs
new file mode 100644
index 0000000..b1ddba2
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectResponseContract.cs
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------
+// <copyright file="IHttpDirectResponseContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+
+ /// <summary>
+ /// Contract class for the <see cref="IHttpDirectResponse"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IHttpDirectResponse))]
+ public abstract class IHttpDirectResponseContract : IHttpDirectResponse {
+ #region IHttpDirectResponse Members
+
+ /// <summary>
+ /// Gets the HTTP status code that the direct response should be sent with.
+ /// </summary>
+ /// <value></value>
+ HttpStatusCode IHttpDirectResponse.HttpStatusCode {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the HTTP headers to add to the response.
+ /// </summary>
+ /// <value>May be an empty collection, but must not be <c>null</c>.</value>
+ WebHeaderCollection IHttpDirectResponse.Headers {
+ get {
+ Contract.Ensures(Contract.Result<WebHeaderCollection>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpIndirectResponse.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpIndirectResponse.cs
new file mode 100644
index 0000000..7d0fe0c
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IHttpIndirectResponse.cs
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------
+// <copyright file="IHttpIndirectResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System.Diagnostics.Contracts;
+ using System.Net;
+
+ /// <summary>
+ /// An interface that allows indirect response messages to specify
+ /// HTTP transport specific properties.
+ /// </summary>
+ public interface IHttpIndirectResponse {
+ /// <summary>
+ /// Gets a value indicating whether the payload for the message should be included
+ /// in the redirect fragment instead of the query string or POST entity.
+ /// </summary>
+ bool Include301RedirectPayloadInFragment { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessage.cs b/src/DotNetOpenAuth.Core/Messaging/IMessage.cs
new file mode 100644
index 0000000..e91a160
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IMessage.cs
@@ -0,0 +1,100 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Text;
+
+ /// <summary>
+ /// The interface that classes must implement to be serialized/deserialized
+ /// as protocol or extension messages.
+ /// </summary>
+ [ContractClass(typeof(IMessageContract))]
+ public interface IMessage {
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ Version Version { get; }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IDictionary<string, string> ExtraData { get; }
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void EnsureValidMessage();
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IMessage"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IMessage))]
+ internal abstract class IMessageContract : IMessage {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IMessageContract"/> class from being created.
+ /// </summary>
+ private IMessageContract() {
+ }
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ Version IMessage.Version {
+ get {
+ Contract.Ensures(Contract.Result<Version>() != null);
+ return default(Version); // dummy return
+ }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IDictionary<string, string> IMessage.ExtraData {
+ get {
+ Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null);
+ return default(IDictionary<string, string>);
+ }
+ }
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void IMessage.EnsureValidMessage() {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageFactory.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageFactory.cs
new file mode 100644
index 0000000..b44bbbf
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IMessageFactory.cs
@@ -0,0 +1,93 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessageFactory.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A tool to analyze an incoming message to figure out what concrete class
+ /// is designed to deserialize it and instantiates that class.
+ /// </summary>
+ [ContractClass(typeof(IMessageFactoryContract))]
+ public interface IMessageFactory {
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="recipient">The intended or actual recipient of the request message.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields);
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="request">
+ /// The message that was sent as a request that resulted in the response.
+ /// </param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IMessageFactory"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IMessageFactory))]
+ internal abstract class IMessageFactoryContract : IMessageFactory {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IMessageFactoryContract"/> class from being created.
+ /// </summary>
+ private IMessageFactoryContract() {
+ }
+
+ #region IMessageFactory Members
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="recipient">The intended or actual recipient of the request message.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ IDirectedProtocolMessage IMessageFactory.GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) {
+ Requires.NotNull(recipient, "recipient");
+ Requires.NotNull(fields, "fields");
+
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="request">The message that was sent as a request that resulted in the response.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ IDirectResponseProtocolMessage IMessageFactory.GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(fields, "fields");
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageOriginalPayload.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageOriginalPayload.cs
new file mode 100644
index 0000000..d18be20
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IMessageOriginalPayload.cs
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessageOriginalPayload.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Text;
+
+ /// <summary>
+ /// An interface that appears on messages that need to retain a description of
+ /// what their literal payload was when they were deserialized.
+ /// </summary>
+ [ContractClass(typeof(IMessageOriginalPayloadContract))]
+ public interface IMessageOriginalPayload {
+ /// <summary>
+ /// Gets or sets the original message parts, before any normalization or default values were assigned.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "By design")]
+ IDictionary<string, string> OriginalPayload { get; set; }
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IMessageOriginalPayload"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IMessageOriginalPayload))]
+ internal abstract class IMessageOriginalPayloadContract : IMessageOriginalPayload {
+ /// <summary>
+ /// Gets or sets the original message parts, before any normalization or default values were assigned.
+ /// </summary>
+ IDictionary<string, string> IMessageOriginalPayload.OriginalPayload {
+ get { throw new NotImplementedException(); }
+ set { throw new NotImplementedException(); }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs
new file mode 100644
index 0000000..32ae227
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IMessageWithBinaryData.cs
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessageWithBinaryData.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// The interface that classes must implement to be serialized/deserialized
+ /// as protocol or extension messages that uses POST multi-part data for binary content.
+ /// </summary>
+ [ContractClass(typeof(IMessageWithBinaryDataContract))]
+ public interface IMessageWithBinaryData : IDirectedProtocolMessage {
+ /// <summary>
+ /// Gets the parts of the message that carry binary data.
+ /// </summary>
+ /// <value>A list of parts. Never null.</value>
+ IList<MultipartPostPart> BinaryData { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether this message should be sent as multi-part POST.
+ /// </summary>
+ bool SendAsMultipart { get; }
+ }
+
+ /// <summary>
+ /// The contract class for the <see cref="IMessageWithBinaryData"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IMessageWithBinaryData))]
+ internal abstract class IMessageWithBinaryDataContract : IMessageWithBinaryData {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IMessageWithBinaryDataContract"/> class from being created.
+ /// </summary>
+ private IMessageWithBinaryDataContract() {
+ }
+
+ #region IMessageWithBinaryData Members
+
+ /// <summary>
+ /// Gets the parts of the message that carry binary data.
+ /// </summary>
+ /// <value>A list of parts. Never null.</value>
+ IList<MultipartPostPart> IMessageWithBinaryData.BinaryData {
+ get {
+ Contract.Ensures(Contract.Result<IList<MultipartPostPart>>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this message should be sent as multi-part POST.
+ /// </summary>
+ bool IMessageWithBinaryData.SendAsMultipart {
+ get { throw new NotImplementedException(); }
+ }
+
+ #endregion
+
+ #region IMessage Properties
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ Version IMessage.Version {
+ get {
+ return default(Version); // dummy return
+ }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IDictionary<string, string> IMessage.ExtraData {
+ get {
+ return default(IDictionary<string, string>);
+ }
+ }
+
+ #endregion
+
+ #region IDirectedProtocolMessage Members
+
+ /// <summary>
+ /// Gets the preferred method of transport for the message.
+ /// </summary>
+ /// <remarks>
+ /// For indirect messages this will likely be GET+POST, which both can be simulated in the user agent:
+ /// the GET with a simple 301 Redirect, and the POST with an HTML form in the response with javascript
+ /// to automate submission.
+ /// </remarks>
+ HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the URL of the intended receiver of this message.
+ /// </summary>
+ Uri IDirectedProtocolMessage.Recipient {
+ get { throw new NotImplementedException(); }
+ }
+
+ #endregion
+
+ #region IProtocolMessage Members
+
+ /// <summary>
+ /// Gets the level of protection this message requires.
+ /// </summary>
+ MessageProtections IProtocolMessage.RequiredProtection {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this is a direct or indirect message.
+ /// </summary>
+ MessageTransport IProtocolMessage.Transport {
+ get { throw new NotImplementedException(); }
+ }
+
+ #endregion
+
+ #region IMessage methods
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void IMessage.EnsureValidMessage() {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IMessageWithEvents.cs b/src/DotNetOpenAuth.Core/Messaging/IMessageWithEvents.cs
new file mode 100644
index 0000000..51e00fc
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IMessageWithEvents.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessageWithEvents.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ /// <summary>
+ /// An interface that messages wishing to perform custom serialization/deserialization
+ /// may implement to be notified of <see cref="Channel"/> events.
+ /// </summary>
+ internal interface IMessageWithEvents : IMessage {
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void OnSending();
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void OnReceiving();
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/IProtocolMessage.cs
new file mode 100644
index 0000000..cf43360
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IProtocolMessage.cs
@@ -0,0 +1,27 @@
+//-----------------------------------------------------------------------
+// <copyright file="IProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ /// <summary>
+ /// The interface that classes must implement to be serialized/deserialized
+ /// as protocol messages.
+ /// </summary>
+ public interface IProtocolMessage : IMessage {
+ /// <summary>
+ /// Gets the level of protection this message requires.
+ /// </summary>
+ MessageProtections RequiredProtection { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether this is a direct or indirect message.
+ /// </summary>
+ MessageTransport Transport { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IProtocolMessageWithExtensions.cs b/src/DotNetOpenAuth.Core/Messaging/IProtocolMessageWithExtensions.cs
new file mode 100644
index 0000000..44c4cbb
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IProtocolMessageWithExtensions.cs
@@ -0,0 +1,116 @@
+//-----------------------------------------------------------------------
+// <copyright file="IProtocolMessageWithExtensions.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A protocol message that supports adding extensions to the payload for transmission.
+ /// </summary>
+ [ContractClass(typeof(IProtocolMessageWithExtensionsContract))]
+ public interface IProtocolMessageWithExtensions : IProtocolMessage {
+ /// <summary>
+ /// Gets the list of extensions that are included with this message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IList<IExtensionMessage> Extensions { get; }
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IProtocolMessageWithExtensions"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IProtocolMessageWithExtensions))]
+ internal abstract class IProtocolMessageWithExtensionsContract : IProtocolMessageWithExtensions {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IProtocolMessageWithExtensionsContract"/> class from being created.
+ /// </summary>
+ private IProtocolMessageWithExtensionsContract() {
+ }
+
+ #region IProtocolMessageWithExtensions Members
+
+ /// <summary>
+ /// Gets the list of extensions that are included with this message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IList<IExtensionMessage> IProtocolMessageWithExtensions.Extensions {
+ get {
+ Contract.Ensures(Contract.Result<IList<IExtensionMessage>>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ #endregion
+
+ #region IProtocolMessage Members
+
+ /// <summary>
+ /// Gets the level of protection this message requires.
+ /// </summary>
+ MessageProtections IProtocolMessage.RequiredProtection {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this is a direct or indirect message.
+ /// </summary>
+ MessageTransport IProtocolMessage.Transport {
+ get { throw new NotImplementedException(); }
+ }
+
+ #endregion
+
+ #region IMessage Members
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ Version IMessage.Version {
+ get {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IDictionary<string, string> IMessage.ExtraData {
+ get {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void IMessage.EnsureValidMessage() {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IStreamSerializingDataBag.cs b/src/DotNetOpenAuth.Core/Messaging/IStreamSerializingDataBag.cs
new file mode 100644
index 0000000..2003f9e
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IStreamSerializingDataBag.cs
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------
+// <copyright file="IStreamSerializingDataBag.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+
+ /// <summary>
+ /// An interface implemented by <see cref="DataBag"/>-derived types that support binary serialization.
+ /// </summary>
+ [ContractClass(typeof(IStreamSerializingDataBaContract))]
+ internal interface IStreamSerializingDataBag {
+ /// <summary>
+ /// Serializes the instance to the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ void Serialize(Stream stream);
+
+ /// <summary>
+ /// Initializes the fields on this instance from the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ void Deserialize(Stream stream);
+ }
+
+ /// <summary>
+ /// Code Contract for the <see cref="IStreamSerializingDataBag"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IStreamSerializingDataBag))]
+ internal abstract class IStreamSerializingDataBaContract : IStreamSerializingDataBag {
+ /// <summary>
+ /// Serializes the instance to the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ void IStreamSerializingDataBag.Serialize(Stream stream) {
+ Contract.Requires(stream != null);
+ Contract.Requires(stream.CanWrite);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Initializes the fields on this instance from the specified stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ void IStreamSerializingDataBag.Deserialize(Stream stream) {
+ Contract.Requires(stream != null);
+ Contract.Requires(stream.CanRead);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/ITamperResistantProtocolMessage.cs b/src/DotNetOpenAuth.Core/Messaging/ITamperResistantProtocolMessage.cs
new file mode 100644
index 0000000..0da6303
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/ITamperResistantProtocolMessage.cs
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------
+// <copyright file="ITamperResistantProtocolMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ /// <summary>
+ /// The contract a message that is signed must implement.
+ /// </summary>
+ /// <remarks>
+ /// This type might have appeared in the DotNetOpenAuth.Messaging.Bindings namespace since
+ /// it is only used by types in that namespace, but all those types are internal and this
+ /// is the only one that was public.
+ /// </remarks>
+ public interface ITamperResistantProtocolMessage : IProtocolMessage {
+ /// <summary>
+ /// Gets or sets the message signature.
+ /// </summary>
+ string Signature { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs
new file mode 100644
index 0000000..90d2f1f
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponse.cs
@@ -0,0 +1,191 @@
+//-----------------------------------------------------------------------
+// <copyright file="IncomingWebResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+
+ /// <summary>
+ /// Details on the incoming response from a direct web request to a remote party.
+ /// </summary>
+ [ContractVerification(true)]
+ [ContractClass(typeof(IncomingWebResponseContract))]
+ public abstract class IncomingWebResponse : IDisposable {
+ /// <summary>
+ /// The encoding to use in reading a response that does not declare its own content encoding.
+ /// </summary>
+ private const string DefaultContentEncoding = "ISO-8859-1";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class.
+ /// </summary>
+ protected internal IncomingWebResponse() {
+ this.Status = HttpStatusCode.OK;
+ this.Headers = new WebHeaderCollection();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class.
+ /// </summary>
+ /// <param name="requestUri">The original request URI.</param>
+ /// <param name="response">The response to initialize from. The network stream is used by this class directly.</param>
+ protected IncomingWebResponse(Uri requestUri, HttpWebResponse response) {
+ Requires.NotNull(requestUri, "requestUri");
+ Requires.NotNull(response, "response");
+
+ this.RequestUri = requestUri;
+ if (!string.IsNullOrEmpty(response.ContentType)) {
+ try {
+ this.ContentType = new ContentType(response.ContentType);
+ } catch (FormatException) {
+ Logger.Messaging.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", response.ResponseUri.AbsoluteUri, response.ContentType);
+ }
+ }
+ this.ContentEncoding = string.IsNullOrEmpty(response.ContentEncoding) ? DefaultContentEncoding : response.ContentEncoding;
+ this.FinalUri = response.ResponseUri;
+ this.Status = response.StatusCode;
+ this.Headers = response.Headers;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IncomingWebResponse"/> class.
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ /// <param name="responseUri">The final URI to respond to the request.</param>
+ /// <param name="headers">The headers.</param>
+ /// <param name="statusCode">The status code.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="contentEncoding">The content encoding.</param>
+ protected IncomingWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers, HttpStatusCode statusCode, string contentType, string contentEncoding) {
+ Requires.NotNull(requestUri, "requestUri");
+
+ this.RequestUri = requestUri;
+ this.Status = statusCode;
+ if (!string.IsNullOrEmpty(contentType)) {
+ try {
+ this.ContentType = new ContentType(contentType);
+ } catch (FormatException) {
+ Logger.Messaging.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", responseUri.AbsoluteUri, contentType);
+ }
+ }
+ this.ContentEncoding = string.IsNullOrEmpty(contentEncoding) ? DefaultContentEncoding : contentEncoding;
+ this.Headers = headers;
+ this.FinalUri = responseUri;
+ }
+
+ /// <summary>
+ /// Gets the type of the content.
+ /// </summary>
+ public ContentType ContentType { get; private set; }
+
+ /// <summary>
+ /// Gets the content encoding.
+ /// </summary>
+ public string ContentEncoding { get; private set; }
+
+ /// <summary>
+ /// Gets the URI of the initial request.
+ /// </summary>
+ public Uri RequestUri { get; private set; }
+
+ /// <summary>
+ /// Gets the URI that finally responded to the request.
+ /// </summary>
+ /// <remarks>
+ /// This can be different from the <see cref="RequestUri"/> in cases of
+ /// redirection during the request.
+ /// </remarks>
+ public Uri FinalUri { get; internal set; }
+
+ /// <summary>
+ /// Gets the headers that must be included in the response to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// The headers in this collection are not meant to be a comprehensive list
+ /// of exactly what should be sent, but are meant to augment whatever headers
+ /// are generally included in a typical response.
+ /// </remarks>
+ public WebHeaderCollection Headers { get; internal set; }
+
+ /// <summary>
+ /// Gets the HTTP status code to use in the HTTP response.
+ /// </summary>
+ public HttpStatusCode Status { get; internal set; }
+
+ /// <summary>
+ /// Gets the body of the HTTP response.
+ /// </summary>
+ public abstract Stream ResponseStream { get; }
+
+ /// <summary>
+ /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override string ToString() {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "RequestUri = {0}", this.RequestUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ResponseUri = {0}", this.FinalUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "StatusCode = {0}", this.Status));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentType = {0}", this.ContentType));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentEncoding = {0}", this.ContentEncoding));
+ sb.AppendLine("Headers:");
+ foreach (string header in this.Headers) {
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "\t{0}: {1}", header, this.Headers[header]));
+ }
+
+ return sb.ToString();
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Creates a text reader for the response stream.
+ /// </summary>
+ /// <returns>The text reader, initialized for the proper encoding.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")]
+ public abstract StreamReader GetResponseReader();
+
+ /// <summary>
+ /// Gets an offline snapshot version of this instance.
+ /// </summary>
+ /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param>
+ /// <returns>A snapshot version of this instance.</returns>
+ /// <remarks>
+ /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot
+ /// will automatically close and dispose of the underlying response stream.
+ /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will
+ /// be the self same instance.
+ /// </remarks>
+ internal abstract CachedDirectWebResponse GetSnapshot(int maximumBytesToCache);
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ Stream responseStream = this.ResponseStream;
+ if (responseStream != null) {
+ responseStream.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponseContract.cs b/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponseContract.cs
new file mode 100644
index 0000000..8c9a6df
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/IncomingWebResponseContract.cs
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------
+// <copyright file="IncomingWebResponseContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+
+ /// <summary>
+ /// Code contract for the <see cref="IncomingWebResponse"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(IncomingWebResponse))]
+ internal abstract class IncomingWebResponseContract : IncomingWebResponse {
+ /// <summary>
+ /// Gets the body of the HTTP response.
+ /// </summary>
+ /// <value></value>
+ public override Stream ResponseStream {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Creates a text reader for the response stream.
+ /// </summary>
+ /// <returns>
+ /// The text reader, initialized for the proper encoding.
+ /// </returns>
+ public override StreamReader GetResponseReader() {
+ Contract.Ensures(Contract.Result<StreamReader>() != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets an offline snapshot version of this instance.
+ /// </summary>
+ /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param>
+ /// <returns>A snapshot version of this instance.</returns>
+ /// <remarks>
+ /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot
+ /// will automatically close and dispose of the underlying response stream.
+ /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will
+ /// be the self same instance.
+ /// </remarks>
+ internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) {
+ Requires.InRange(maximumBytesToCache >= 0, "maximumBytesToCache");
+ Requires.ValidState(this.RequestUri != null);
+ Contract.Ensures(Contract.Result<CachedDirectWebResponse>() != null);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/InternalErrorException.cs b/src/DotNetOpenAuth.Core/Messaging/InternalErrorException.cs
new file mode 100644
index 0000000..32b44f2
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/InternalErrorException.cs
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------
+// <copyright file="InternalErrorException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// An internal exception to throw if an internal error within the library requires
+ /// an abort of the operation.
+ /// </summary>
+ /// <remarks>
+ /// This exception is internal to prevent clients of the library from catching what is
+ /// really an unexpected, potentially unrecoverable exception.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic", Justification = "We want this to be internal so clients cannot catch it.")]
+ [Serializable]
+ internal class InternalErrorException : Exception {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InternalErrorException"/> class.
+ /// </summary>
+ public InternalErrorException() { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InternalErrorException"/> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public InternalErrorException(string message) : base(message) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InternalErrorException"/> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="inner">The inner exception.</param>
+ public InternalErrorException(string message, Exception inner) : base(message, inner) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InternalErrorException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The <paramref name="info"/> parameter is null.
+ /// </exception>
+ /// <exception cref="T:System.Runtime.Serialization.SerializationException">
+ /// The class name is null or <see cref="P:System.Exception.HResult"/> is zero (0).
+ /// </exception>
+ protected InternalErrorException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/KeyedCollectionDelegate.cs b/src/DotNetOpenAuth.Core/Messaging/KeyedCollectionDelegate.cs
new file mode 100644
index 0000000..c0a08df
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/KeyedCollectionDelegate.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+// <copyright file="KeyedCollectionDelegate.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A KeyedCollection whose item -&gt; key transform is provided via a delegate
+ /// to its constructor, and null items are disallowed.
+ /// </summary>
+ /// <typeparam name="TKey">The type of the key.</typeparam>
+ /// <typeparam name="TItem">The type of the item.</typeparam>
+ [Serializable]
+ internal class KeyedCollectionDelegate<TKey, TItem> : KeyedCollection<TKey, TItem> {
+ /// <summary>
+ /// The delegate that returns a key for the given item.
+ /// </summary>
+ private Func<TItem, TKey> getKeyForItemDelegate;
+
+ /// <summary>
+ /// Initializes a new instance of the KeyedCollectionDelegate class.
+ /// </summary>
+ /// <param name="getKeyForItemDelegate">The delegate that gets the key for a given item.</param>
+ internal KeyedCollectionDelegate(Func<TItem, TKey> getKeyForItemDelegate) {
+ Requires.NotNull(getKeyForItemDelegate, "getKeyForItemDelegate");
+
+ this.getKeyForItemDelegate = getKeyForItemDelegate;
+ }
+
+ /// <summary>
+ /// When implemented in a derived class, extracts the key from the specified element.
+ /// </summary>
+ /// <param name="item">The element from which to extract the key.</param>
+ /// <returns>The key for the specified element.</returns>
+ protected override TKey GetKeyForItem(TItem item) {
+ ErrorUtilities.VerifyArgumentNotNull(item, "item"); // null items not supported.
+ return this.getKeyForItemDelegate(item);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs b/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs
new file mode 100644
index 0000000..22c660c
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessagePartAttribute.cs
@@ -0,0 +1,121 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagePartAttribute.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Net.Security;
+ using System.Reflection;
+
+ /// <summary>
+ /// Applied to fields and properties that form a key/value in a protocol message.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = true)]
+ [DebuggerDisplay("MessagePartAttribute {Name}")]
+ public sealed class MessagePartAttribute : Attribute {
+ /// <summary>
+ /// The overridden name to use as the serialized name for the property.
+ /// </summary>
+ private string name;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessagePartAttribute"/> class.
+ /// </summary>
+ public MessagePartAttribute() {
+ this.AllowEmpty = true;
+ this.MinVersionValue = new Version(0, 0);
+ this.MaxVersionValue = new Version(int.MaxValue, 0);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessagePartAttribute"/> class.
+ /// </summary>
+ /// <param name="name">
+ /// A special name to give the value of this member in the serialized message.
+ /// When null or empty, the name of the member will be used in the serialized message.
+ /// </param>
+ public MessagePartAttribute(string name)
+ : this() {
+ this.Name = name;
+ }
+
+ /// <summary>
+ /// Gets the name of the serialized form of this member in the message.
+ /// </summary>
+ public string Name {
+ get { return this.name; }
+ private set { this.name = string.IsNullOrEmpty(value) ? null : value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the level of protection required by this member in the serialized message.
+ /// </summary>
+ /// <remarks>
+ /// Message part protection must be provided and verified by the channel binding element(s)
+ /// that provide security.
+ /// </remarks>
+ public ProtectionLevel RequiredProtection { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this member is a required part of the serialized message.
+ /// </summary>
+ public bool IsRequired { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the string value is allowed to be empty in the serialized message.
+ /// </summary>
+ /// <value>Default is true.</value>
+ public bool AllowEmpty { get; set; }
+
+ /// <summary>
+ /// Gets or sets an IMessagePartEncoder custom encoder to use
+ /// to translate the applied member to and from a string.
+ /// </summary>
+ public Type Encoder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum version of the protocol this attribute applies to
+ /// and overrides any attributes with lower values for this property.
+ /// </summary>
+ /// <value>Defaults to 0.0.</value>
+ public string MinVersion {
+ get { return this.MinVersionValue.ToString(); }
+ set { this.MinVersionValue = new Version(value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum version of the protocol this attribute applies to.
+ /// </summary>
+ /// <value>Defaults to int.MaxValue for the major version number.</value>
+ /// <remarks>
+ /// Specifying <see cref="MinVersion"/> on another attribute on the same member
+ /// automatically turns this attribute off. This property should only be set when
+ /// a property is totally dropped from a newer version of the protocol.
+ /// </remarks>
+ public string MaxVersion {
+ get { return this.MaxVersionValue.ToString(); }
+ set { this.MaxVersionValue = new Version(value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the minimum version of the protocol this attribute applies to
+ /// and overrides any attributes with lower values for this property.
+ /// </summary>
+ /// <value>Defaults to 0.0.</value>
+ internal Version MinVersionValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum version of the protocol this attribute applies to.
+ /// </summary>
+ /// <value>Defaults to int.MaxValue for the major version number.</value>
+ /// <remarks>
+ /// Specifying <see cref="MinVersion"/> on another attribute on the same member
+ /// automatically turns this attribute off. This property should only be set when
+ /// a property is totally dropped from a newer version of the protocol.
+ /// </remarks>
+ internal Version MaxVersionValue { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageProtections.cs b/src/DotNetOpenAuth.Core/Messaging/MessageProtections.cs
new file mode 100644
index 0000000..c78c92f
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessageProtections.cs
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageProtections.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// Categorizes the various types of channel binding elements so they can be properly ordered.
+ /// </summary>
+ /// <remarks>
+ /// The order of these enum values is significant.
+ /// Each successive value requires the protection offered by all the previous values
+ /// in order to be reliable. For example, message expiration is meaningless without
+ /// tamper protection to prevent a user from changing the timestamp on a message.
+ /// </remarks>
+ [Flags]
+ public enum MessageProtections {
+ /// <summary>
+ /// No protection.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// A binding element that signs a message before sending and validates its signature upon receiving.
+ /// </summary>
+ TamperProtection = 0x1,
+
+ /// <summary>
+ /// A binding element that enforces a maximum message age between sending and processing on the receiving side.
+ /// </summary>
+ Expiration = 0x2,
+
+ /// <summary>
+ /// A binding element that prepares messages for replay detection and detects replayed messages on the receiving side.
+ /// </summary>
+ ReplayProtection = 0x4,
+
+ /// <summary>
+ /// All forms of protection together.
+ /// </summary>
+ All = TamperProtection | Expiration | ReplayProtection,
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageReceivingEndpoint.cs b/src/DotNetOpenAuth.Core/Messaging/MessageReceivingEndpoint.cs
new file mode 100644
index 0000000..ca7c5df
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessageReceivingEndpoint.cs
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageReceivingEndpoint.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// An immutable description of a URL that receives messages.
+ /// </summary>
+ [DebuggerDisplay("{AllowedMethods} {Location}")]
+ [Serializable]
+ public class MessageReceivingEndpoint {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageReceivingEndpoint"/> class.
+ /// </summary>
+ /// <param name="locationUri">The URL of this endpoint.</param>
+ /// <param name="method">The HTTP method(s) allowed.</param>
+ public MessageReceivingEndpoint(string locationUri, HttpDeliveryMethods method)
+ : this(new Uri(locationUri), method) {
+ Requires.NotNull(locationUri, "locationUri");
+ Requires.InRange(method != HttpDeliveryMethods.None, "method");
+ Requires.InRange((method & HttpDeliveryMethods.HttpVerbMask) != 0, "method", MessagingStrings.GetOrPostFlagsRequired);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageReceivingEndpoint"/> class.
+ /// </summary>
+ /// <param name="location">The URL of this endpoint.</param>
+ /// <param name="method">The HTTP method(s) allowed.</param>
+ public MessageReceivingEndpoint(Uri location, HttpDeliveryMethods method) {
+ Requires.NotNull(location, "location");
+ Requires.InRange(method != HttpDeliveryMethods.None, "method");
+ Requires.InRange((method & HttpDeliveryMethods.HttpVerbMask) != 0, "method", MessagingStrings.GetOrPostFlagsRequired);
+
+ this.Location = location;
+ this.AllowedMethods = method;
+ }
+
+ /// <summary>
+ /// Gets the URL of this endpoint.
+ /// </summary>
+ public Uri Location { get; private set; }
+
+ /// <summary>
+ /// Gets the HTTP method(s) allowed.
+ /// </summary>
+ public HttpDeliveryMethods AllowedMethods { get; private set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs
new file mode 100644
index 0000000..957ea41
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs
@@ -0,0 +1,236 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageSerializer.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Reflection;
+ using System.Xml;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// Serializes/deserializes OAuth messages for/from transit.
+ /// </summary>
+ [ContractVerification(true)]
+ internal class MessageSerializer {
+ /// <summary>
+ /// The specific <see cref="IMessage"/>-derived type
+ /// that will be serialized and deserialized using this class.
+ /// </summary>
+ private readonly Type messageType;
+
+ /// <summary>
+ /// Initializes a new instance of the MessageSerializer class.
+ /// </summary>
+ /// <param name="messageType">The specific <see cref="IMessage"/>-derived type
+ /// that will be serialized and deserialized using this class.</param>
+ [ContractVerification(false)] // bugs/limitations in CC static analysis
+ private MessageSerializer(Type messageType) {
+ Requires.NotNullSubtype<IMessage>(messageType, "messageType");
+ Contract.Ensures(this.messageType != null);
+ this.messageType = messageType;
+ }
+
+ /// <summary>
+ /// Creates or reuses a message serializer for a given message type.
+ /// </summary>
+ /// <param name="messageType">The type of message that will be serialized/deserialized.</param>
+ /// <returns>A message serializer for the given message type.</returns>
+ [ContractVerification(false)] // bugs/limitations in CC static analysis
+ internal static MessageSerializer Get(Type messageType) {
+ Requires.NotNullSubtype<IMessage>(messageType, "messageType");
+
+ return new MessageSerializer(messageType);
+ }
+
+ /// <summary>
+ /// Reads JSON as a flat dictionary into a message.
+ /// </summary>
+ /// <param name="messageDictionary">The message dictionary to fill with the JSON-deserialized data.</param>
+ /// <param name="reader">The JSON reader.</param>
+ internal static void DeserializeJsonAsFlatDictionary(IDictionary<string, string> messageDictionary, XmlDictionaryReader reader) {
+ Requires.NotNull(messageDictionary, "messageDictionary");
+ Requires.NotNull(reader, "reader");
+
+ reader.Read(); // one extra one to skip the root node.
+ while (reader.Read()) {
+ if (reader.NodeType == XmlNodeType.EndElement) {
+ // This is likely the closing </root> tag.
+ continue;
+ }
+
+ string key = reader.Name;
+ reader.Read();
+ string value = reader.ReadContentAsString();
+ messageDictionary[key] = value;
+ }
+ }
+
+ /// <summary>
+ /// Reads the data from a message instance and writes a XML/JSON encoding of it.
+ /// </summary>
+ /// <param name="messageDictionary">The message to be serialized.</param>
+ /// <param name="writer">The writer to use for the serialized form.</param>
+ /// <remarks>
+ /// Use <see cref="System.Runtime.Serialization.Json.JsonReaderWriterFactory.CreateJsonWriter(System.IO.Stream)"/>
+ /// to create the <see cref="XmlDictionaryWriter"/> instance capable of emitting JSON.
+ /// </remarks>
+ [Pure]
+ internal static void Serialize(MessageDictionary messageDictionary, XmlDictionaryWriter writer) {
+ Requires.NotNull(messageDictionary, "messageDictionary");
+ Requires.NotNull(writer, "writer");
+
+ writer.WriteStartElement("root");
+ writer.WriteAttributeString("type", "object");
+ foreach (var pair in messageDictionary) {
+ bool include = false;
+ string type = "string";
+ MessagePart partDescription;
+ if (messageDictionary.Description.Mapping.TryGetValue(pair.Key, out partDescription)) {
+ Contract.Assume(partDescription != null);
+ if (partDescription.IsRequired || partDescription.IsNondefaultValueSet(messageDictionary.Message)) {
+ include = true;
+ if (IsNumeric(partDescription.MemberDeclaredType)) {
+ type = "number";
+ } else if (partDescription.MemberDeclaredType.IsAssignableFrom(typeof(bool))) {
+ type = "boolean";
+ }
+ }
+ } else {
+ // This is extra data. We always write it out.
+ include = true;
+ }
+
+ if (include) {
+ writer.WriteStartElement(pair.Key);
+ writer.WriteAttributeString("type", type);
+ writer.WriteString(pair.Value);
+ writer.WriteEndElement();
+ }
+ }
+
+ writer.WriteEndElement();
+ }
+
+ /// <summary>
+ /// Reads XML/JSON into a message dictionary.
+ /// </summary>
+ /// <param name="messageDictionary">The message to deserialize into.</param>
+ /// <param name="reader">The XML/JSON to read into the message.</param>
+ /// <exception cref="ProtocolException">Thrown when protocol rules are broken by the incoming message.</exception>
+ /// <remarks>
+ /// Use <see cref="System.Runtime.Serialization.Json.JsonReaderWriterFactory.CreateJsonReader(System.IO.Stream, System.Xml.XmlDictionaryReaderQuotas)"/>
+ /// to create the <see cref="XmlDictionaryReader"/> instance capable of reading JSON.
+ /// </remarks>
+ internal static void Deserialize(MessageDictionary messageDictionary, XmlDictionaryReader reader) {
+ Requires.NotNull(messageDictionary, "messageDictionary");
+ Requires.NotNull(reader, "reader");
+
+ DeserializeJsonAsFlatDictionary(messageDictionary, reader);
+
+ // Make sure all the required parts are present and valid.
+ messageDictionary.Description.EnsureMessagePartsPassBasicValidation(messageDictionary);
+ messageDictionary.Message.EnsureValidMessage();
+ }
+
+ /// <summary>
+ /// Reads the data from a message instance and returns a series of name=value pairs for the fields that must be included in the message.
+ /// </summary>
+ /// <param name="messageDictionary">The message to be serialized.</param>
+ /// <returns>The dictionary of values to send for the message.</returns>
+ [Pure]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Parallel design with Deserialize method.")]
+ internal IDictionary<string, string> Serialize(MessageDictionary messageDictionary) {
+ Requires.NotNull(messageDictionary, "messageDictionary");
+ Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null);
+
+ // Rather than hand back the whole message dictionary (which
+ // includes keys with blank values), create a new dictionary
+ // that only has required keys, and optional keys whose
+ // values are not empty (or default).
+ var result = new Dictionary<string, string>();
+ foreach (var pair in messageDictionary) {
+ MessagePart partDescription;
+ if (messageDictionary.Description.Mapping.TryGetValue(pair.Key, out partDescription)) {
+ Contract.Assume(partDescription != null);
+ if (partDescription.IsRequired || partDescription.IsNondefaultValueSet(messageDictionary.Message)) {
+ result.Add(pair.Key, pair.Value);
+ }
+ } else {
+ // This is extra data. We always write it out.
+ result.Add(pair.Key, pair.Value);
+ }
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Reads name=value pairs into a message.
+ /// </summary>
+ /// <param name="fields">The name=value pairs that were read in from the transport.</param>
+ /// <param name="messageDictionary">The message to deserialize into.</param>
+ /// <exception cref="ProtocolException">Thrown when protocol rules are broken by the incoming message.</exception>
+ internal void Deserialize(IDictionary<string, string> fields, MessageDictionary messageDictionary) {
+ Requires.NotNull(fields, "fields");
+ Requires.NotNull(messageDictionary, "messageDictionary");
+
+ var messageDescription = messageDictionary.Description;
+
+ // Before we deserialize the message, make sure all the required parts are present.
+ messageDescription.EnsureMessagePartsPassBasicValidation(fields);
+
+ try {
+ foreach (var pair in fields) {
+ messageDictionary[pair.Key] = pair.Value;
+ }
+ } catch (ArgumentException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.ErrorDeserializingMessage, this.messageType.Name);
+ }
+
+ messageDictionary.Message.EnsureValidMessage();
+
+ var originalPayloadMessage = messageDictionary.Message as IMessageOriginalPayload;
+ if (originalPayloadMessage != null) {
+ originalPayloadMessage.OriginalPayload = fields;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified type is numeric.
+ /// </summary>
+ /// <param name="type">The type to test.</param>
+ /// <returns>
+ /// <c>true</c> if the specified type is numeric; otherwise, <c>false</c>.
+ /// </returns>
+ private static bool IsNumeric(Type type) {
+ return type.IsAssignableFrom(typeof(double))
+ || type.IsAssignableFrom(typeof(float))
+ || type.IsAssignableFrom(typeof(short))
+ || type.IsAssignableFrom(typeof(int))
+ || type.IsAssignableFrom(typeof(long))
+ || type.IsAssignableFrom(typeof(ushort))
+ || type.IsAssignableFrom(typeof(uint))
+ || type.IsAssignableFrom(typeof(ulong));
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [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.messageType != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageTransport.cs b/src/DotNetOpenAuth.Core/Messaging/MessageTransport.cs
new file mode 100644
index 0000000..ee06c95
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessageTransport.cs
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageTransport.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ /// <summary>
+ /// The type of transport mechanism used for a message: either direct or indirect.
+ /// </summary>
+ public enum MessageTransport {
+ /// <summary>
+ /// A message that is sent directly from the Consumer to the Service Provider, or vice versa.
+ /// </summary>
+ Direct,
+
+ /// <summary>
+ /// A message that is sent from one party to another via a redirect in the user agent.
+ /// </summary>
+ Indirect,
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Messaging.cd b/src/DotNetOpenAuth.Core/Messaging/Messaging.cd
new file mode 100644
index 0000000..0c22565
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Messaging.cd
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1" GroupingSetting="Access">
+ <Class Name="DotNetOpenAuth.Messaging.Channel">
+ <Position X="5.25" Y="0.75" Width="1.75" />
+ <Compartments>
+ <Compartment Name="Protected" Collapsed="true" />
+ <Compartment Name="Internal" Collapsed="true" />
+ <Compartment Name="Private" Collapsed="true" />
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>gBgQgAIAAQAEAIgAAEAAAARBIAAQgAAQEEAAAABAMAA=</HashCode>
+ <FileName>Messaging\Channel.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsCollectionAssociation>
+ <Property Name="BindingElements" />
+ </ShowAsCollectionAssociation>
+ </Class>
+ <Interface Name="DotNetOpenAuth.Messaging.IChannelBindingElement">
+ <Position X="1.75" Y="1.5" Width="2.25" />
+ <TypeIdentifier>
+ <HashCode>BAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAEAAAAAAAAA=</HashCode>
+ <FileName>Messaging\IChannelBindingElement.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Property Name="Protection" />
+ </ShowAsAssociation>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.Messaging.IProtocolMessage">
+ <Position X="5.25" Y="3.5" Width="1.75" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAQAAAAAAAAAAAAAYAAAAAAAAAAAACAAAAAAA=</HashCode>
+ <FileName>Messaging\IProtocolMessage.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Property Name="RequiredProtection" />
+ <Property Name="Transport" />
+ </ShowAsAssociation>
+ </Interface>
+ <Interface Name="DotNetOpenAuth.Messaging.IDirectedProtocolMessage">
+ <Position X="5" Y="5.25" Width="2.25" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\IDirectedProtocolMessage.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Enum Name="DotNetOpenAuth.Messaging.MessageProtection">
+ <Position X="2" Y="3.5" Width="1.75" />
+ <TypeIdentifier>
+ <HashCode>AIAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAA=</HashCode>
+ <FileName>Messaging\MessageProtection.cs</FileName>
+ </TypeIdentifier>
+ </Enum>
+ <Enum Name="DotNetOpenAuth.Messaging.MessageTransport">
+ <Position X="8" Y="3.5" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAACAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>Messaging\MessageTransport.cs</FileName>
+ </TypeIdentifier>
+ </Enum>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs
new file mode 100644
index 0000000..11bd751
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs
@@ -0,0 +1,675 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class MessagingStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal MessagingStrings() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.Messaging.MessagingStrings", typeof(MessagingStrings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Argument&apos;s {0}.{1} property is required but is empty or null..
+ /// </summary>
+ internal static string ArgumentPropertyMissing {
+ get {
+ return ResourceManager.GetString("ArgumentPropertyMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false..
+ /// </summary>
+ internal static string BinaryDataRequiresMultipart {
+ get {
+ return ResourceManager.GetString("BinaryDataRequiresMultipart", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed..
+ /// </summary>
+ internal static string CurrentHttpContextRequired {
+ get {
+ return ResourceManager.GetString("CurrentHttpContextRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to DataContractSerializer could not be initialized on message type {0}. Is it missing a [DataContract] attribute?.
+ /// </summary>
+ internal static string DataContractMissingFromMessageType {
+ get {
+ return ResourceManager.GetString("DataContractMissingFromMessageType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to DataContractSerializer could not be initialized on message type {0} because the DataContractAttribute.Namespace property is not set..
+ /// </summary>
+ internal static string DataContractMissingNamespace {
+ get {
+ return ResourceManager.GetString("DataContractMissingNamespace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An instance of type {0} was expected, but received unexpected derived type {1}..
+ /// </summary>
+ internal static string DerivedTypeNotExpected {
+ get {
+ return ResourceManager.GetString("DerivedTypeNotExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The directed message&apos;s Recipient property must not be null..
+ /// </summary>
+ internal static string DirectedMessageMissingRecipient {
+ get {
+ return ResourceManager.GetString("DirectedMessageMissingRecipient", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The given set of options is not supported by this web request handler..
+ /// </summary>
+ internal static string DirectWebRequestOptionsNotSupported {
+ get {
+ return ResourceManager.GetString("DirectWebRequestOptionsNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to instantiate the message part encoder/decoder type {0}..
+ /// </summary>
+ internal static string EncoderInstantiationFailed {
+ get {
+ return ResourceManager.GetString("EncoderInstantiationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error while deserializing message {0}..
+ /// </summary>
+ internal static string ErrorDeserializingMessage {
+ get {
+ return ResourceManager.GetString("ErrorDeserializingMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error occurred while sending a direct message or getting the response..
+ /// </summary>
+ internal static string ErrorInRequestReplyMessage {
+ get {
+ return ResourceManager.GetString("ErrorInRequestReplyMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This exception was not constructed with a root request message that caused it..
+ /// </summary>
+ internal static string ExceptionNotConstructedForTransit {
+ get {
+ return ResourceManager.GetString("ExceptionNotConstructedForTransit", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This exception must be instantiated with a recipient that will receive the error message, or a direct request message instance that this exception will respond to..
+ /// </summary>
+ internal static string ExceptionUndeliverable {
+ get {
+ return ResourceManager.GetString("ExceptionUndeliverable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected {0} message but received no recognizable message..
+ /// </summary>
+ internal static string ExpectedMessageNotReceived {
+ get {
+ return ResourceManager.GetString("ExpectedMessageNotReceived", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The message expired at {0} and it is now {1}..
+ /// </summary>
+ internal static string ExpiredMessage {
+ get {
+ return ResourceManager.GetString("ExpiredMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to add extra parameter &apos;{0}&apos; with value &apos;{1}&apos;..
+ /// </summary>
+ internal static string ExtraParameterAddFailure {
+ get {
+ return ResourceManager.GetString("ExtraParameterAddFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to At least one of GET or POST flags must be present..
+ /// </summary>
+ internal static string GetOrPostFlagsRequired {
+ get {
+ return ResourceManager.GetString("GetOrPostFlagsRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This method requires a current HttpContext. Alternatively, use an overload of this method that allows you to pass in information without an HttpContext..
+ /// </summary>
+ internal static string HttpContextRequired {
+ get {
+ return ResourceManager.GetString("HttpContextRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Messages that indicate indirect transport must implement the {0} interface..
+ /// </summary>
+ internal static string IndirectMessagesMustImplementIDirectedProtocolMessage {
+ get {
+ return ResourceManager.GetString("IndirectMessagesMustImplementIDirectedProtocolMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insecure web request for &apos;{0}&apos; aborted due to security requirements demanding HTTPS..
+ /// </summary>
+ internal static string InsecureWebRequestWithSslRequired {
+ get {
+ return ResourceManager.GetString("InsecureWebRequestWithSslRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} message required protections {{{1}}} but the channel could only apply {{{2}}}..
+ /// </summary>
+ internal static string InsufficientMessageProtection {
+ get {
+ return ResourceManager.GetString("InsufficientMessageProtection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The customized binding element ordering is invalid..
+ /// </summary>
+ internal static string InvalidCustomBindingElementOrder {
+ get {
+ return ResourceManager.GetString("InvalidCustomBindingElementOrder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Some part(s) of the message have invalid values: {0}.
+ /// </summary>
+ internal static string InvalidMessageParts {
+ get {
+ return ResourceManager.GetString("InvalidMessageParts", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The incoming message had an invalid or missing nonce..
+ /// </summary>
+ internal static string InvalidNonceReceived {
+ get {
+ return ResourceManager.GetString("InvalidNonceReceived", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An item with the same key has already been added..
+ /// </summary>
+ internal static string KeyAlreadyExists {
+ get {
+ return ResourceManager.GetString("KeyAlreadyExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} message does not support extensions..
+ /// </summary>
+ internal static string MessageNotExtensible {
+ get {
+ return ResourceManager.GetString("MessageNotExtensible", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value for {0}.{1} on member {1} was expected to derive from {2} but was {3}..
+ /// </summary>
+ internal static string MessagePartEncoderWrongType {
+ get {
+ return ResourceManager.GetString("MessagePartEncoderWrongType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error while reading message &apos;{0}&apos; parameter &apos;{1}&apos; with value &apos;{2}&apos;..
+ /// </summary>
+ internal static string MessagePartReadFailure {
+ get {
+ return ResourceManager.GetString("MessagePartReadFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Message parameter &apos;{0}&apos; with value &apos;{1}&apos; failed to base64 decode..
+ /// </summary>
+ internal static string MessagePartValueBase64DecodingFault {
+ get {
+ return ResourceManager.GetString("MessagePartValueBase64DecodingFault", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error while preparing message &apos;{0}&apos; parameter &apos;{1}&apos; for sending..
+ /// </summary>
+ internal static string MessagePartWriteFailure {
+ get {
+ return ResourceManager.GetString("MessagePartWriteFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This message has a timestamp of {0}, which is beyond the allowable clock skew for in the future..
+ /// </summary>
+ internal static string MessageTimestampInFuture {
+ get {
+ return ResourceManager.GetString("MessageTimestampInFuture", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A non-empty string was expected..
+ /// </summary>
+ internal static string NonEmptyStringExpected {
+ get {
+ return ResourceManager.GetString("NonEmptyStringExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A message response is already queued for sending in the response stream..
+ /// </summary>
+ internal static string QueuedMessageResponseAlreadyExists {
+ get {
+ return ResourceManager.GetString("QueuedMessageResponseAlreadyExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This message has already been processed. This could indicate a replay attack in progress..
+ /// </summary>
+ internal static string ReplayAttackDetected {
+ get {
+ return ResourceManager.GetString("ReplayAttackDetected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This channel does not support replay protection..
+ /// </summary>
+ internal static string ReplayProtectionNotSupported {
+ get {
+ return ResourceManager.GetString("ReplayProtectionNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The following message parts had constant value requirements that were unsatisfied: {0}.
+ /// </summary>
+ internal static string RequiredMessagePartConstantIncorrect {
+ get {
+ return ResourceManager.GetString("RequiredMessagePartConstantIncorrect", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The following required non-empty parameters were empty in the {0} message: {1}.
+ /// </summary>
+ internal static string RequiredNonEmptyParameterWasEmpty {
+ get {
+ return ResourceManager.GetString("RequiredNonEmptyParameterWasEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The following required parameters were missing from the {0} message: {1}.
+ /// </summary>
+ internal static string RequiredParametersMissing {
+ get {
+ return ResourceManager.GetString("RequiredParametersMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding element offering the {0} protection requires other protection that is not provided..
+ /// </summary>
+ internal static string RequiredProtectionMissing {
+ get {
+ return ResourceManager.GetString("RequiredProtectionMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The list is empty..
+ /// </summary>
+ internal static string SequenceContainsNoElements {
+ get {
+ return ResourceManager.GetString("SequenceContainsNoElements", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The list contains a null element..
+ /// </summary>
+ internal static string SequenceContainsNullElement {
+ get {
+ return ResourceManager.GetString("SequenceContainsNullElement", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An HttpContext.Current.Session object is required..
+ /// </summary>
+ internal static string SessionRequired {
+ get {
+ return ResourceManager.GetString("SessionRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Message signature was incorrect..
+ /// </summary>
+ internal static string SignatureInvalid {
+ get {
+ return ResourceManager.GetString("SignatureInvalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This channel does not support signing messages. To support signing messages, a derived Channel type must override the Sign and IsSignatureValid methods..
+ /// </summary>
+ internal static string SigningNotSupported {
+ get {
+ return ResourceManager.GetString("SigningNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This message factory does not support message type(s): {0}.
+ /// </summary>
+ internal static string StandardMessageFactoryUnsupportedMessageType {
+ get {
+ return ResourceManager.GetString("StandardMessageFactoryUnsupportedMessageType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream must have a known length..
+ /// </summary>
+ internal static string StreamMustHaveKnownLength {
+ get {
+ return ResourceManager.GetString("StreamMustHaveKnownLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream&apos;s CanRead property returned false..
+ /// </summary>
+ internal static string StreamUnreadable {
+ get {
+ return ResourceManager.GetString("StreamUnreadable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream&apos;s CanWrite property returned false..
+ /// </summary>
+ internal static string StreamUnwritable {
+ get {
+ return ResourceManager.GetString("StreamUnwritable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected at most 1 binding element to apply the {0} protection, but more than one applied..
+ /// </summary>
+ internal static string TooManyBindingsOfferingSameProtection {
+ get {
+ return ResourceManager.GetString("TooManyBindingsOfferingSameProtection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The maximum allowable number of redirects were exceeded while requesting &apos;{0}&apos;..
+ /// </summary>
+ internal static string TooManyRedirects {
+ get {
+ return ResourceManager.GetString("TooManyRedirects", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The array must not be empty..
+ /// </summary>
+ internal static string UnexpectedEmptyArray {
+ get {
+ return ResourceManager.GetString("UnexpectedEmptyArray", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The empty string is not allowed..
+ /// </summary>
+ internal static string UnexpectedEmptyString {
+ get {
+ return ResourceManager.GetString("UnexpectedEmptyString", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected direct response to use HTTP status code {0} but was {1} instead..
+ /// </summary>
+ internal static string UnexpectedHttpStatusCode {
+ get {
+ return ResourceManager.GetString("UnexpectedHttpStatusCode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Message parameter &apos;{0}&apos; had unexpected value &apos;{1}&apos;..
+ /// </summary>
+ internal static string UnexpectedMessagePartValue {
+ get {
+ return ResourceManager.GetString("UnexpectedMessagePartValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected message {0} parameter &apos;{1}&apos; to have value &apos;{2}&apos; but had &apos;{3}&apos; instead..
+ /// </summary>
+ internal static string UnexpectedMessagePartValueForConstant {
+ get {
+ return ResourceManager.GetString("UnexpectedMessagePartValueForConstant", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected message {0} but received {1} instead..
+ /// </summary>
+ internal static string UnexpectedMessageReceived {
+ get {
+ return ResourceManager.GetString("UnexpectedMessageReceived", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected message type received..
+ /// </summary>
+ internal static string UnexpectedMessageReceivedOfMany {
+ get {
+ return ResourceManager.GetString("UnexpectedMessageReceivedOfMany", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A null key was included and is not allowed..
+ /// </summary>
+ internal static string UnexpectedNullKey {
+ get {
+ return ResourceManager.GetString("UnexpectedNullKey", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A null or empty key was included and is not allowed..
+ /// </summary>
+ internal static string UnexpectedNullOrEmptyKey {
+ get {
+ return ResourceManager.GetString("UnexpectedNullOrEmptyKey", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A null value was included for key &apos;{0}&apos; and is not allowed..
+ /// </summary>
+ internal static string UnexpectedNullValue {
+ get {
+ return ResourceManager.GetString("UnexpectedNullValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} or a derived type was expected, but {1} was given..
+ /// </summary>
+ internal static string UnexpectedType {
+ get {
+ return ResourceManager.GetString("UnexpectedType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} property has unrecognized value {1}..
+ /// </summary>
+ internal static string UnrecognizedEnumValue {
+ get {
+ return ResourceManager.GetString("UnrecognizedEnumValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The URL &apos;{0}&apos; is rated unsafe and cannot be requested this way..
+ /// </summary>
+ internal static string UnsafeWebRequestDetected {
+ get {
+ return ResourceManager.GetString("UnsafeWebRequestDetected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This blob is not a recognized encryption format..
+ /// </summary>
+ internal static string UnsupportedEncryptionAlgorithm {
+ get {
+ return ResourceManager.GetString("UnsupportedEncryptionAlgorithm", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The HTTP verb &apos;{0}&apos; is unrecognized and unsupported..
+ /// </summary>
+ internal static string UnsupportedHttpVerb {
+ get {
+ return ResourceManager.GetString("UnsupportedHttpVerb", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; messages cannot be received with HTTP verb &apos;{1}&apos;..
+ /// </summary>
+ internal static string UnsupportedHttpVerbForMessageType {
+ get {
+ return ResourceManager.GetString("UnsupportedHttpVerbForMessageType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Redirects on POST requests that are to untrusted servers is not supported..
+ /// </summary>
+ internal static string UntrustedRedirectsOnPOSTNotSupported {
+ get {
+ return ResourceManager.GetString("UntrustedRedirectsOnPOSTNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Web request to &apos;{0}&apos; failed..
+ /// </summary>
+ internal static string WebRequestFailed {
+ get {
+ return ResourceManager.GetString("WebRequestFailed", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx
new file mode 100644
index 0000000..bd10b76
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx
@@ -0,0 +1,324 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentPropertyMissing" xml:space="preserve">
+ <value>Argument's {0}.{1} property is required but is empty or null.</value>
+ </data>
+ <data name="CurrentHttpContextRequired" xml:space="preserve">
+ <value>HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed.</value>
+ </data>
+ <data name="DataContractMissingFromMessageType" xml:space="preserve">
+ <value>DataContractSerializer could not be initialized on message type {0}. Is it missing a [DataContract] attribute?</value>
+ </data>
+ <data name="DataContractMissingNamespace" xml:space="preserve">
+ <value>DataContractSerializer could not be initialized on message type {0} because the DataContractAttribute.Namespace property is not set.</value>
+ </data>
+ <data name="DerivedTypeNotExpected" xml:space="preserve">
+ <value>An instance of type {0} was expected, but received unexpected derived type {1}.</value>
+ </data>
+ <data name="DirectedMessageMissingRecipient" xml:space="preserve">
+ <value>The directed message's Recipient property must not be null.</value>
+ </data>
+ <data name="DirectWebRequestOptionsNotSupported" xml:space="preserve">
+ <value>The given set of options is not supported by this web request handler.</value>
+ </data>
+ <data name="ErrorDeserializingMessage" xml:space="preserve">
+ <value>Error while deserializing message {0}.</value>
+ </data>
+ <data name="ErrorInRequestReplyMessage" xml:space="preserve">
+ <value>Error occurred while sending a direct message or getting the response.</value>
+ </data>
+ <data name="ExceptionNotConstructedForTransit" xml:space="preserve">
+ <value>This exception was not constructed with a root request message that caused it.</value>
+ </data>
+ <data name="ExpectedMessageNotReceived" xml:space="preserve">
+ <value>Expected {0} message but received no recognizable message.</value>
+ </data>
+ <data name="ExpiredMessage" xml:space="preserve">
+ <value>The message expired at {0} and it is now {1}.</value>
+ </data>
+ <data name="GetOrPostFlagsRequired" xml:space="preserve">
+ <value>At least one of GET or POST flags must be present.</value>
+ </data>
+ <data name="HttpContextRequired" xml:space="preserve">
+ <value>This method requires a current HttpContext. Alternatively, use an overload of this method that allows you to pass in information without an HttpContext.</value>
+ </data>
+ <data name="IndirectMessagesMustImplementIDirectedProtocolMessage" xml:space="preserve">
+ <value>Messages that indicate indirect transport must implement the {0} interface.</value>
+ </data>
+ <data name="InsecureWebRequestWithSslRequired" xml:space="preserve">
+ <value>Insecure web request for '{0}' aborted due to security requirements demanding HTTPS.</value>
+ </data>
+ <data name="InsufficientMessageProtection" xml:space="preserve">
+ <value>The {0} message required protections {{{1}}} but the channel could only apply {{{2}}}.</value>
+ </data>
+ <data name="InvalidCustomBindingElementOrder" xml:space="preserve">
+ <value>The customized binding element ordering is invalid.</value>
+ </data>
+ <data name="InvalidMessageParts" xml:space="preserve">
+ <value>Some part(s) of the message have invalid values: {0}</value>
+ </data>
+ <data name="InvalidNonceReceived" xml:space="preserve">
+ <value>The incoming message had an invalid or missing nonce.</value>
+ </data>
+ <data name="KeyAlreadyExists" xml:space="preserve">
+ <value>An item with the same key has already been added.</value>
+ </data>
+ <data name="MessageNotExtensible" xml:space="preserve">
+ <value>The {0} message does not support extensions.</value>
+ </data>
+ <data name="MessagePartEncoderWrongType" xml:space="preserve">
+ <value>The value for {0}.{1} on member {1} was expected to derive from {2} but was {3}.</value>
+ </data>
+ <data name="MessagePartReadFailure" xml:space="preserve">
+ <value>Error while reading message '{0}' parameter '{1}' with value '{2}'.</value>
+ </data>
+ <data name="MessagePartValueBase64DecodingFault" xml:space="preserve">
+ <value>Message parameter '{0}' with value '{1}' failed to base64 decode.</value>
+ </data>
+ <data name="MessagePartWriteFailure" xml:space="preserve">
+ <value>Error while preparing message '{0}' parameter '{1}' for sending.</value>
+ </data>
+ <data name="QueuedMessageResponseAlreadyExists" xml:space="preserve">
+ <value>A message response is already queued for sending in the response stream.</value>
+ </data>
+ <data name="ReplayAttackDetected" xml:space="preserve">
+ <value>This message has already been processed. This could indicate a replay attack in progress.</value>
+ </data>
+ <data name="ReplayProtectionNotSupported" xml:space="preserve">
+ <value>This channel does not support replay protection.</value>
+ </data>
+ <data name="RequiredNonEmptyParameterWasEmpty" xml:space="preserve">
+ <value>The following required non-empty parameters were empty in the {0} message: {1}</value>
+ </data>
+ <data name="RequiredParametersMissing" xml:space="preserve">
+ <value>The following required parameters were missing from the {0} message: {1}</value>
+ </data>
+ <data name="RequiredProtectionMissing" xml:space="preserve">
+ <value>The binding element offering the {0} protection requires other protection that is not provided.</value>
+ </data>
+ <data name="SequenceContainsNoElements" xml:space="preserve">
+ <value>The list is empty.</value>
+ </data>
+ <data name="SequenceContainsNullElement" xml:space="preserve">
+ <value>The list contains a null element.</value>
+ </data>
+ <data name="SignatureInvalid" xml:space="preserve">
+ <value>Message signature was incorrect.</value>
+ </data>
+ <data name="SigningNotSupported" xml:space="preserve">
+ <value>This channel does not support signing messages. To support signing messages, a derived Channel type must override the Sign and IsSignatureValid methods.</value>
+ </data>
+ <data name="StreamUnreadable" xml:space="preserve">
+ <value>The stream's CanRead property returned false.</value>
+ </data>
+ <data name="StreamUnwritable" xml:space="preserve">
+ <value>The stream's CanWrite property returned false.</value>
+ </data>
+ <data name="TooManyBindingsOfferingSameProtection" xml:space="preserve">
+ <value>Expected at most 1 binding element to apply the {0} protection, but more than one applied.</value>
+ </data>
+ <data name="TooManyRedirects" xml:space="preserve">
+ <value>The maximum allowable number of redirects were exceeded while requesting '{0}'.</value>
+ </data>
+ <data name="UnexpectedEmptyArray" xml:space="preserve">
+ <value>The array must not be empty.</value>
+ </data>
+ <data name="UnexpectedEmptyString" xml:space="preserve">
+ <value>The empty string is not allowed.</value>
+ </data>
+ <data name="UnexpectedMessagePartValue" xml:space="preserve">
+ <value>Message parameter '{0}' had unexpected value '{1}'.</value>
+ </data>
+ <data name="UnexpectedMessagePartValueForConstant" xml:space="preserve">
+ <value>Expected message {0} parameter '{1}' to have value '{2}' but had '{3}' instead.</value>
+ </data>
+ <data name="UnexpectedMessageReceived" xml:space="preserve">
+ <value>Expected message {0} but received {1} instead.</value>
+ </data>
+ <data name="UnexpectedMessageReceivedOfMany" xml:space="preserve">
+ <value>Unexpected message type received.</value>
+ </data>
+ <data name="UnexpectedNullKey" xml:space="preserve">
+ <value>A null key was included and is not allowed.</value>
+ </data>
+ <data name="UnexpectedNullOrEmptyKey" xml:space="preserve">
+ <value>A null or empty key was included and is not allowed.</value>
+ </data>
+ <data name="UnexpectedNullValue" xml:space="preserve">
+ <value>A null value was included for key '{0}' and is not allowed.</value>
+ </data>
+ <data name="UnexpectedType" xml:space="preserve">
+ <value>The type {0} or a derived type was expected, but {1} was given.</value>
+ </data>
+ <data name="UnrecognizedEnumValue" xml:space="preserve">
+ <value>{0} property has unrecognized value {1}.</value>
+ </data>
+ <data name="UnsafeWebRequestDetected" xml:space="preserve">
+ <value>The URL '{0}' is rated unsafe and cannot be requested this way.</value>
+ </data>
+ <data name="UntrustedRedirectsOnPOSTNotSupported" xml:space="preserve">
+ <value>Redirects on POST requests that are to untrusted servers is not supported.</value>
+ </data>
+ <data name="WebRequestFailed" xml:space="preserve">
+ <value>Web request to '{0}' failed.</value>
+ </data>
+ <data name="ExceptionUndeliverable" xml:space="preserve">
+ <value>This exception must be instantiated with a recipient that will receive the error message, or a direct request message instance that this exception will respond to.</value>
+ </data>
+ <data name="UnsupportedHttpVerbForMessageType" xml:space="preserve">
+ <value>'{0}' messages cannot be received with HTTP verb '{1}'.</value>
+ </data>
+ <data name="UnexpectedHttpStatusCode" xml:space="preserve">
+ <value>Expected direct response to use HTTP status code {0} but was {1} instead.</value>
+ </data>
+ <data name="UnsupportedHttpVerb" xml:space="preserve">
+ <value>The HTTP verb '{0}' is unrecognized and unsupported.</value>
+ </data>
+ <data name="NonEmptyStringExpected" xml:space="preserve">
+ <value>A non-empty string was expected.</value>
+ </data>
+ <data name="StreamMustHaveKnownLength" xml:space="preserve">
+ <value>The stream must have a known length.</value>
+ </data>
+ <data name="BinaryDataRequiresMultipart" xml:space="preserve">
+ <value>Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.</value>
+ </data>
+ <data name="SessionRequired" xml:space="preserve">
+ <value>An HttpContext.Current.Session object is required.</value>
+ </data>
+ <data name="StandardMessageFactoryUnsupportedMessageType" xml:space="preserve">
+ <value>This message factory does not support message type(s): {0}</value>
+ </data>
+ <data name="RequiredMessagePartConstantIncorrect" xml:space="preserve">
+ <value>The following message parts had constant value requirements that were unsatisfied: {0}</value>
+ </data>
+ <data name="EncoderInstantiationFailed" xml:space="preserve">
+ <value>Unable to instantiate the message part encoder/decoder type {0}.</value>
+ </data>
+ <data name="MessageTimestampInFuture" xml:space="preserve">
+ <value>This message has a timestamp of {0}, which is beyond the allowable clock skew for in the future.</value>
+ </data>
+ <data name="UnsupportedEncryptionAlgorithm" xml:space="preserve">
+ <value>This blob is not a recognized encryption format.</value>
+ </data>
+ <data name="ExtraParameterAddFailure" xml:space="preserve">
+ <value>Failed to add extra parameter '{0}' with value '{1}'.</value>
+ </data>
+</root>
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.sr.resx b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.sr.resx
new file mode 100644
index 0000000..5b7b716
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.sr.resx
@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentPropertyMissing" xml:space="preserve">
+ <value>Svojstvo {0}.{1} argumenta je neophodno, ali je ono prazno ili nepostojeće.</value>
+ </data>
+ <data name="CurrentHttpContextRequired" xml:space="preserve">
+ <value>HttpContext.Current je nepostojeći. Mora postojati ASP.NET zahtev u procesu da bi ova operacija bila uspešna.</value>
+ </data>
+ <data name="DataContractMissingFromMessageType" xml:space="preserve">
+ <value>DataContractSerializer se ne može inicijalizovati na tipu poruke {0}. Da li nedostaje [DataContract] atribut?</value>
+ </data>
+ <data name="DataContractMissingNamespace" xml:space="preserve">
+ <value>DataContractSerializer se ne može inicijalizovati na tipu poruke {0} jer svojstvo DataContractAttribute.Namespace property nije podešeno.</value>
+ </data>
+ <data name="DerivedTypeNotExpected" xml:space="preserve">
+ <value>Instanca tipa {0} je bila očekivana, a primljena je neočekivana izvedena instanca tipa {1}.</value>
+ </data>
+ <data name="DirectedMessageMissingRecipient" xml:space="preserve">
+ <value>Svojstvo Recipient usmerene poruke ne sme biti nepostojeće.</value>
+ </data>
+ <data name="DirectWebRequestOptionsNotSupported" xml:space="preserve">
+ <value>Dati set opcija ({0}) nije podržan od strane {1}.</value>
+ </data>
+ <data name="ErrorDeserializingMessage" xml:space="preserve">
+ <value>Greška prilikom deserijalizacije poruke {0}.</value>
+ </data>
+ <data name="ErrorInRequestReplyMessage" xml:space="preserve">
+ <value>Greška se desila tokom slanja usmerene poruke ili tokom primanja odgovora.</value>
+ </data>
+ <data name="ExceptionNotConstructedForTransit" xml:space="preserve">
+ <value>Ovaj izuzetak nije napravljen sa početnom porukom koja ga je izazvala.</value>
+ </data>
+ <data name="ExpectedMessageNotReceived" xml:space="preserve">
+ <value>Očekivana je poruka {0} a primljena poruka nije prepoznata.</value>
+ </data>
+ <data name="ExpiredMessage" xml:space="preserve">
+ <value>Poruka ističe u {0} a sada je {1}.</value>
+ </data>
+ <data name="GetOrPostFlagsRequired" xml:space="preserve">
+ <value>Bar jedan od GET ili POST flegova mora biti prisutan.</value>
+ </data>
+ <data name="HttpContextRequired" xml:space="preserve">
+ <value>Ovaj metod zahteva tekući HttpContext. Kao alternativa, koristite preklopljeni metod koji dozvoljava da se prosledi informacija bez HttpContext-a.</value>
+ </data>
+ <data name="IndirectMessagesMustImplementIDirectedProtocolMessage" xml:space="preserve">
+ <value>Poruke koje ukazuju na indirektni transport moraju implementirati {0} interfejs.</value>
+ </data>
+ <data name="InsecureWebRequestWithSslRequired" xml:space="preserve">
+ <value>Nebezbedan web zahtev za '{0}' prekinut zbog bezbednosnih zahteva koji zahtevaju HTTPS.</value>
+ </data>
+ <data name="InsufficientMessageProtection" xml:space="preserve">
+ <value>Poruka {0} je zahtevala zaštite {{{1}}} ali prenosni kanal nije mogao primeniti {{{2}}}.</value>
+ </data>
+ <data name="InvalidCustomBindingElementOrder" xml:space="preserve">
+ <value>Redosled prilagođenih vezujućih elemenata je neispravan.</value>
+ </data>
+ <data name="InvalidMessageParts" xml:space="preserve">
+ <value>Neki deo ili delovi poruke imaju nevalidne vrednosti: {0}</value>
+ </data>
+ <data name="InvalidNonceReceived" xml:space="preserve">
+ <value>Primljena poruka imala je neispravan ili nedostajući jedinstveni identifikator.</value>
+ </data>
+ <data name="KeyAlreadyExists" xml:space="preserve">
+ <value>Element sa istom vrednošću ključa je već dodat.</value>
+ </data>
+ <data name="MessageNotExtensible" xml:space="preserve">
+ <value>Poruka {0} ne podržava ekstenzije.</value>
+ </data>
+ <data name="MessagePartEncoderWrongType" xml:space="preserve">
+ <value>Vrednost za {0}.{1} člana {1} je trebala da bude izvedena od {2} ali je izvedena od {3}.</value>
+ </data>
+ <data name="MessagePartReadFailure" xml:space="preserve">
+ <value>Greška prilikom čitanja poruke '{0}' parametar '{1}' sa vrednošću '{2}'.</value>
+ </data>
+ <data name="MessagePartValueBase64DecodingFault" xml:space="preserve">
+ <value>Parametar poruke '{0}' sa vrednošću '{1}' nije se base64-dekodovao.</value>
+ </data>
+ <data name="MessagePartWriteFailure" xml:space="preserve">
+ <value>Greška prilikom pripremanja poruke '{0}' parametra '{1}' za slanje.</value>
+ </data>
+ <data name="QueuedMessageResponseAlreadyExists" xml:space="preserve">
+ <value>Poruka-odgovor je već u redu za slanje u stream-u za odgovore.</value>
+ </data>
+ <data name="ReplayAttackDetected" xml:space="preserve">
+ <value>Ova poruka je već obrađena. Ovo može ukazivati na replay napad u toku.</value>
+ </data>
+ <data name="ReplayProtectionNotSupported" xml:space="preserve">
+ <value>Ovaj kanal ne podržava replay zaštitu.</value>
+ </data>
+ <data name="RequiredNonEmptyParameterWasEmpty" xml:space="preserve">
+ <value>Sledeći zahtevani parametri koji ne smeju biti prazni su bili prazni u {0} poruke: {1}</value>
+ </data>
+ <data name="RequiredParametersMissing" xml:space="preserve">
+ <value>Sledeći zahtevani parametri nedostaju u {0} poruke: {1}</value>
+ </data>
+ <data name="RequiredProtectionMissing" xml:space="preserve">
+ <value>Povezujući element koji nudi {0} zaštitu zahteva drugu zaštitu koja nije ponuđena.</value>
+ </data>
+ <data name="SequenceContainsNoElements" xml:space="preserve">
+ <value>Lista je prazna.</value>
+ </data>
+ <data name="SequenceContainsNullElement" xml:space="preserve">
+ <value>Lista sadrži prazan (null) element.</value>
+ </data>
+ <data name="SignatureInvalid" xml:space="preserve">
+ <value>Potpis poruke je neispravan.</value>
+ </data>
+ <data name="SigningNotSupported" xml:space="preserve">
+ <value>Ovaj kanal ne podržava potpisivanje poruka. Da bi podržao potpisivanje poruka, izvedeni tip Channel mora preklopiti Sign i IsSignatureValid metode.</value>
+ </data>
+ <data name="StreamUnreadable" xml:space="preserve">
+ <value>Svojstvo stream-a CanRead je vratilo false.</value>
+ </data>
+ <data name="StreamUnwritable" xml:space="preserve">
+ <value>Svojstvo stream-a CanWrite je vratilo false.</value>
+ </data>
+ <data name="TooManyBindingsOfferingSameProtection" xml:space="preserve">
+ <value>Očekivano je da najviše 1 povezujući element primeni zaštitu {0}, ali je više njih primenjeno.</value>
+ </data>
+ <data name="TooManyRedirects" xml:space="preserve">
+ <value>Maksimalno dozvoljeni broj redirekcija je prekoračen u toku zahtevanja '{0}'.</value>
+ </data>
+ <data name="UnexpectedEmptyArray" xml:space="preserve">
+ <value>Niz ne sme biti prazan.</value>
+ </data>
+ <data name="UnexpectedEmptyString" xml:space="preserve">
+ <value>Prazan string nije dozvoljen.</value>
+ </data>
+ <data name="UnexpectedMessagePartValue" xml:space="preserve">
+ <value>Parametar poruke '{0}' ima neočekivanu '{1}'.</value>
+ </data>
+ <data name="UnexpectedMessagePartValueForConstant" xml:space="preserve">
+ <value>Očekivano je da od poruke {0} parametar '{1}' ima vrednost '{2}' ali je imao vrednost '{3}'.</value>
+ </data>
+ <data name="UnexpectedMessageReceived" xml:space="preserve">
+ <value>Očekivana je poruka {0} ali je umesto nje primljena {1}.</value>
+ </data>
+ <data name="UnexpectedMessageReceivedOfMany" xml:space="preserve">
+ <value>Poruka neočekivanog tipa je primljena.</value>
+ </data>
+ <data name="UnexpectedNullKey" xml:space="preserve">
+ <value>null ključ je uključen a nije dozvoljen.</value>
+ </data>
+ <data name="UnexpectedNullOrEmptyKey" xml:space="preserve">
+ <value>null ili prazan ključ je uključen a nije dozvoljen.</value>
+ </data>
+ <data name="UnexpectedNullValue" xml:space="preserve">
+ <value>null vrednost je uključena za ključ '{0}' a nije dozvoljena.</value>
+ </data>
+ <data name="UnexpectedType" xml:space="preserve">
+ <value>Tip {0} ili izvedeni tip je očekivan, a dat je {1}.</value>
+ </data>
+ <data name="UnrecognizedEnumValue" xml:space="preserve">
+ <value>{0} svojstvo ima nepoznatu vrednost {1}.</value>
+ </data>
+ <data name="UnsafeWebRequestDetected" xml:space="preserve">
+ <value>URL '{0}' je rangiran kao nebezbedan i ne može se zahtevati na ovaj način.</value>
+ </data>
+ <data name="UntrustedRedirectsOnPOSTNotSupported" xml:space="preserve">
+ <value>Redirekcije na POST zahteve usmerene ka serverima kojima se ne veruje nisu podržane.</value>
+ </data>
+ <data name="WebRequestFailed" xml:space="preserve">
+ <value>Web zahtev za '{0}' nije uspeo.</value>
+ </data>
+ <data name="ExceptionUndeliverable" xml:space="preserve">
+ <value>Ovaj izuzetak mora se kreirati zajedno sa primaocem koji će primiti poruku o grešci ili sa instancom poruke direktnog zahteva na koju će ovaj izuzetak odgovoriti.</value>
+ </data>
+ <data name="UnsupportedHttpVerbForMessageType" xml:space="preserve">
+ <value>'{0}' poruka ne može biti primljeno sa HTTP glagolom '{1}'.</value>
+ </data>
+ <data name="UnexpectedHttpStatusCode" xml:space="preserve">
+ <value>Očekivano je da direktan odgovor koristi HTTP status kod {0} a korišćen je {1}.</value>
+ </data>
+ <data name="UnsupportedHttpVerb" xml:space="preserve">
+ <value>HTTP glagol '{0}' je neprepoznat i nije podržan.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs
new file mode 100644
index 0000000..9277734
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs
@@ -0,0 +1,1709 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagingUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.IO.Compression;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Security;
+ using System.Security.Cryptography;
+ using System.Text;
+ using System.Web;
+ using System.Web.Mvc;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A grab-bag of utility methods useful for the channel stack of the protocol.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Utility class touches lots of surface area")]
+ public static class MessagingUtilities {
+ /// <summary>
+ /// The cryptographically strong random data generator used for creating secrets.
+ /// </summary>
+ /// <remarks>The random number generator is thread-safe.</remarks>
+ internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();
+
+ /// <summary>
+ /// A pseudo-random data generator (NOT cryptographically strong random data)
+ /// </summary>
+ internal static readonly Random NonCryptoRandomDataGenerator = new Random();
+
+ /// <summary>
+ /// The uppercase alphabet.
+ /// </summary>
+ internal const string UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ /// <summary>
+ /// The lowercase alphabet.
+ /// </summary>
+ internal const string LowercaseLetters = "abcdefghijklmnopqrstuvwxyz";
+
+ /// <summary>
+ /// The set of base 10 digits.
+ /// </summary>
+ internal const string Digits = "0123456789";
+
+ /// <summary>
+ /// The set of digits and alphabetic letters (upper and lowercase).
+ /// </summary>
+ internal const string AlphaNumeric = UppercaseLetters + LowercaseLetters + Digits;
+
+ /// <summary>
+ /// All the characters that are allowed for use as a base64 encoding character.
+ /// </summary>
+ internal const string Base64Characters = AlphaNumeric + "+" + "/";
+
+ /// <summary>
+ /// All the characters that are allowed for use as a base64 encoding character
+ /// in the "web safe" context.
+ /// </summary>
+ internal const string Base64WebSafeCharacters = AlphaNumeric + "-" + "_";
+
+ /// <summary>
+ /// The set of digits, and alphabetic letters (upper and lowercase) that are clearly
+ /// visually distinguishable.
+ /// </summary>
+ internal const string AlphaNumericNoLookAlikes = "23456789abcdefghjkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ";
+
+ /// <summary>
+ /// The length of private symmetric secret handles.
+ /// </summary>
+ /// <remarks>
+ /// This value needn't be high, as we only expect to have a small handful of unexpired secrets at a time,
+ /// and handle recycling is permissible.
+ /// </remarks>
+ private const int SymmetricSecretHandleLength = 4;
+
+ /// <summary>
+ /// The default lifetime of a private secret.
+ /// </summary>
+ private static readonly TimeSpan SymmetricSecretKeyLifespan = Configuration.DotNetOpenAuthSection.Messaging.PrivateSecretMaximumAge;
+
+ /// <summary>
+ /// A character array containing just the = character.
+ /// </summary>
+ private static readonly char[] EqualsArray = new char[] { '=' };
+
+ /// <summary>
+ /// A character array containing just the , character.
+ /// </summary>
+ private static readonly char[] CommaArray = new char[] { ',' };
+
+ /// <summary>
+ /// A character array containing just the " character.
+ /// </summary>
+ private static readonly char[] QuoteArray = new char[] { '"' };
+
+ /// <summary>
+ /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986.
+ /// </summary>
+ private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" };
+
+ /// <summary>
+ /// A set of escaping mappings that help secure a string from javscript execution.
+ /// </summary>
+ /// <remarks>
+ /// The characters to escape here are inspired by
+ /// http://code.google.com/p/doctype/wiki/ArticleXSSInJavaScript
+ /// </remarks>
+ private static readonly Dictionary<string, string> javascriptStaticStringEscaping = new Dictionary<string, string> {
+ { "\\", @"\\" }, // this WAS just above the & substitution but we moved it here to prevent double-escaping
+ { "\t", @"\t" },
+ { "\n", @"\n" },
+ { "\r", @"\r" },
+ { "\u0085", @"\u0085" },
+ { "\u2028", @"\u2028" },
+ { "\u2029", @"\u2029" },
+ { "'", @"\x27" },
+ { "\"", @"\x22" },
+ { "&", @"\x26" },
+ { "<", @"\x3c" },
+ { ">", @"\x3e" },
+ { "=", @"\x3d" },
+ };
+
+ /// <summary>
+ /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult.
+ /// </summary>
+ /// <param name="response">The response to send to the user agent.</param>
+ /// <returns>The <see cref="ActionResult"/> instance to be returned by the Controller's action method.</returns>
+ public static ActionResult AsActionResult(this OutgoingWebResponse response) {
+ Requires.NotNull(response, "response");
+ return new OutgoingWebResponseActionResult(response);
+ }
+
+ /// <summary>
+ /// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any.
+ /// Cookieless session directory (if applicable) is also included.
+ /// </summary>
+ /// <returns>The URL in the user agent's Location bar.</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "The Uri merging requires use of a string value.")]
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Expensive call should not be a property.")]
+ public static Uri GetRequestUrlFromContext() {
+ Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ HttpContext context = HttpContext.Current;
+
+ return HttpRequestInfo.GetPublicFacingUrl(context.Request, context.Request.ServerVariables);
+ }
+
+ /// <summary>
+ /// Strips any and all URI query parameters that start with some prefix.
+ /// </summary>
+ /// <param name="uri">The URI that may have a query with parameters to remove.</param>
+ /// <param name="prefix">The prefix for parameters to remove. A period is NOT automatically appended.</param>
+ /// <returns>Either a new Uri with the parameters removed if there were any to remove, or the same Uri instance if no parameters needed to be removed.</returns>
+ public static Uri StripQueryArgumentsWithPrefix(this Uri uri, string prefix) {
+ Requires.NotNull(uri, "uri");
+ Requires.NotNullOrEmpty(prefix, "prefix");
+
+ NameValueCollection queryArgs = HttpUtility.ParseQueryString(uri.Query);
+ var matchingKeys = queryArgs.Keys.OfType<string>().Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
+ if (matchingKeys.Count > 0) {
+ UriBuilder builder = new UriBuilder(uri);
+ foreach (string key in matchingKeys) {
+ queryArgs.Remove(key);
+ }
+ builder.Query = CreateQueryString(queryArgs.ToDictionary());
+ return builder.Uri;
+ } else {
+ return uri;
+ }
+ }
+
+ /// <summary>
+ /// Sends a multipart HTTP POST request (useful for posting files).
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="parts">The parts to include in the POST entity.</param>
+ /// <returns>The HTTP response.</returns>
+ public static IncomingWebResponse PostMultipart(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(requestHandler, "requestHandler");
+ Requires.NotNull(parts, "parts");
+
+ PostMultipartNoGetResponse(request, requestHandler, parts);
+ return requestHandler.GetResponse(request);
+ }
+
+ /// <summary>
+ /// Assembles a message comprised of the message on a given exception and all inner exceptions.
+ /// </summary>
+ /// <param name="exception">The exception.</param>
+ /// <returns>The assembled message.</returns>
+ public static string ToStringDescriptive(this Exception exception) {
+ // The input being null is probably bad, but since this method is called
+ // from a catch block, we don't really want to throw a new exception and
+ // hide the details of this one.
+ if (exception == null) {
+ Logger.Messaging.Error("MessagingUtilities.GetAllMessages called with null input.");
+ }
+
+ StringBuilder message = new StringBuilder();
+ while (exception != null) {
+ message.Append(exception.Message);
+ exception = exception.InnerException;
+ if (exception != null) {
+ message.Append(" ");
+ }
+ }
+
+ return message.ToString();
+ }
+
+ /// <summary>
+ /// Flattens the specified sequence of sequences.
+ /// </summary>
+ /// <typeparam name="T">The type of element contained in the sequence.</typeparam>
+ /// <param name="sequence">The sequence of sequences to flatten.</param>
+ /// <returns>A sequence of the contained items.</returns>
+ [Obsolete("Use Enumerable.SelectMany instead.")]
+ public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence) {
+ ErrorUtilities.VerifyArgumentNotNull(sequence, "sequence");
+
+ foreach (IEnumerable<T> subsequence in sequence) {
+ foreach (T item in subsequence) {
+ yield return item;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Cuts off precision beyond a second on a DateTime value.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>A DateTime with a 0 millisecond component.</returns>
+ public static DateTime CutToSecond(this DateTime value) {
+ return value - TimeSpan.FromMilliseconds(value.Millisecond);
+ }
+
+ /// <summary>
+ /// Adds a name-value pair to the end of a given URL
+ /// as part of the querystring piece. Prefixes a ? or &amp; before
+ /// first element as necessary.
+ /// </summary>
+ /// <param name="builder">The UriBuilder to add arguments to.</param>
+ /// <param name="name">The name of the parameter to add.</param>
+ /// <param name="value">The value of the argument.</param>
+ /// <remarks>
+ /// If the parameters to add match names of parameters that already are defined
+ /// in the query string, the existing ones are <i>not</i> replaced.
+ /// </remarks>
+ public static void AppendQueryArgument(this UriBuilder builder, string name, string value) {
+ AppendQueryArgs(builder, new[] { new KeyValuePair<string, string>(name, value) });
+ }
+
+ /// <summary>
+ /// Adds a set of values to a collection.
+ /// </summary>
+ /// <typeparam name="T">The type of value kept in the collection.</typeparam>
+ /// <param name="collection">The collection to add to.</param>
+ /// <param name="values">The values to add to the collection.</param>
+ public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> values) {
+ Requires.NotNull(collection, "collection");
+ Requires.NotNull(values, "values");
+
+ foreach (var value in values) {
+ collection.Add(value);
+ }
+ }
+
+ /// <summary>
+ /// Tests whether two timespans are within reasonable approximation of each other.
+ /// </summary>
+ /// <param name="self">One TimeSpan.</param>
+ /// <param name="other">The other TimeSpan.</param>
+ /// <param name="marginOfError">The allowable margin of error.</param>
+ /// <returns><c>true</c> if the two TimeSpans are within <paramref name="marginOfError"/> of each other.</returns>
+ public static bool Equals(this TimeSpan self, TimeSpan other, TimeSpan marginOfError) {
+ return TimeSpan.FromMilliseconds(Math.Abs((self - other).TotalMilliseconds)) < marginOfError;
+ }
+
+ /// <summary>
+ /// Clears any existing elements in a collection and fills the collection with a given set of values.
+ /// </summary>
+ /// <typeparam name="T">The type of value kept in the collection.</typeparam>
+ /// <param name="collection">The collection to modify.</param>
+ /// <param name="values">The new values to fill the collection.</param>
+ internal static void ResetContents<T>(this ICollection<T> collection, IEnumerable<T> values) {
+ Requires.NotNull(collection, "collection");
+
+ collection.Clear();
+ if (values != null) {
+ AddRange(collection, values);
+ }
+ }
+
+ /// <summary>
+ /// Strips any and all URI query parameters that serve as parts of a message.
+ /// </summary>
+ /// <param name="uri">The URI that may contain query parameters to remove.</param>
+ /// <param name="messageDescription">The message description whose parts should be removed from the URL.</param>
+ /// <returns>A cleaned URL.</returns>
+ internal static Uri StripMessagePartsFromQueryString(this Uri uri, MessageDescription messageDescription) {
+ Requires.NotNull(uri, "uri");
+ Requires.NotNull(messageDescription, "messageDescription");
+
+ NameValueCollection queryArgs = HttpUtility.ParseQueryString(uri.Query);
+ var matchingKeys = queryArgs.Keys.OfType<string>().Where(key => messageDescription.Mapping.ContainsKey(key)).ToList();
+ if (matchingKeys.Count > 0) {
+ var builder = new UriBuilder(uri);
+ foreach (string key in matchingKeys) {
+ queryArgs.Remove(key);
+ }
+ builder.Query = CreateQueryString(queryArgs.ToDictionary());
+ return builder.Uri;
+ } else {
+ return uri;
+ }
+ }
+
+ /// <summary>
+ /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="parts">The parts to include in the POST entity.</param>
+ internal static void PostMultipartNoGetResponse(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(requestHandler, "requestHandler");
+ Requires.NotNull(parts, "parts");
+
+ Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart");
+ parts = parts.CacheGeneratedResults();
+ string boundary = Guid.NewGuid().ToString();
+ string initialPartLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "--{0}\r\n", boundary);
+ string partLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}\r\n", boundary);
+ string finalTrailingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}--\r\n", boundary);
+ var contentType = new ContentType("multipart/form-data") {
+ Boundary = boundary,
+ CharSet = Channel.PostEntityEncoding.WebName,
+ };
+
+ request.Method = "POST";
+ request.ContentType = contentType.ToString();
+ long contentLength = parts.Sum(p => partLeadingBoundary.Length + p.Length) + finalTrailingBoundary.Length;
+ if (parts.Any()) {
+ contentLength -= 2; // the initial part leading boundary has no leading \r\n
+ }
+ request.ContentLength = contentLength;
+
+ var requestStream = requestHandler.GetRequestStream(request);
+ try {
+ StreamWriter writer = new StreamWriter(requestStream, Channel.PostEntityEncoding);
+ bool firstPart = true;
+ foreach (var part in parts) {
+ writer.Write(firstPart ? initialPartLeadingBoundary : partLeadingBoundary);
+ firstPart = false;
+ part.Serialize(writer);
+ part.Dispose();
+ }
+
+ writer.Write(finalTrailingBoundary);
+ writer.Flush();
+ } 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();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Assembles the content of the HTTP Authorization or WWW-Authenticate header.
+ /// </summary>
+ /// <param name="scheme">The scheme.</param>
+ /// <param name="fields">The fields to include.</param>
+ /// <returns>A value prepared for an HTTP header.</returns>
+ internal static string AssembleAuthorizationHeader(string scheme, IEnumerable<KeyValuePair<string, string>> fields) {
+ Requires.NotNullOrEmpty(scheme, "scheme");
+ Requires.NotNull(fields, "fields");
+
+ var authorization = new StringBuilder();
+ authorization.Append(scheme);
+ authorization.Append(" ");
+ foreach (var pair in fields) {
+ string key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key);
+ string value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value);
+ authorization.Append(key);
+ authorization.Append("=\"");
+ authorization.Append(value);
+ authorization.Append("\",");
+ }
+ authorization.Length--; // remove trailing comma
+ return authorization.ToString();
+ }
+
+ /// <summary>
+ /// Parses the authorization header.
+ /// </summary>
+ /// <param name="scheme">The scheme. Must not be null or empty.</param>
+ /// <param name="authorizationHeader">The authorization header. May be null or empty.</param>
+ /// <returns>A sequence of key=value pairs discovered in the header. Never null, but may be empty.</returns>
+ internal static IEnumerable<KeyValuePair<string, string>> ParseAuthorizationHeader(string scheme, string authorizationHeader) {
+ Requires.NotNullOrEmpty(scheme, "scheme");
+ Contract.Ensures(Contract.Result<IEnumerable<KeyValuePair<string, string>>>() != null);
+
+ string prefix = scheme + " ";
+ if (authorizationHeader != null) {
+ // The authorization header may have multiple sections. Look for the appropriate one.
+ string[] authorizationSections = new string[] { authorizationHeader }; // what is the right delimiter, if any?
+ foreach (string authorization in authorizationSections) {
+ string trimmedAuth = authorization.Trim();
+ if (trimmedAuth.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { // RFC 2617 says this is case INsensitive
+ string data = trimmedAuth.Substring(prefix.Length);
+ return from element in data.Split(CommaArray)
+ let parts = element.Split(EqualsArray, 2)
+ let key = Uri.UnescapeDataString(parts[0])
+ let value = Uri.UnescapeDataString(parts[1].Trim(QuoteArray))
+ select new KeyValuePair<string, string>(key, value);
+ }
+ }
+ }
+
+ return Enumerable.Empty<KeyValuePair<string, string>>();
+ }
+
+ /// <summary>
+ /// Encodes a symmetric key handle and the blob that is encrypted/signed with that key into a single string
+ /// that can be decoded by <see cref="ExtractKeyHandleAndPayload"/>.
+ /// </summary>
+ /// <param name="handle">The cryptographic key handle.</param>
+ /// <param name="payload">The encrypted/signed blob.</param>
+ /// <returns>The combined encoded value.</returns>
+ internal static string CombineKeyHandleAndPayload(string handle, string payload) {
+ Requires.NotNullOrEmpty(handle, "handle");
+ Requires.NotNullOrEmpty(payload, "payload");
+ Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));
+
+ return handle + "!" + payload;
+ }
+
+ /// <summary>
+ /// Extracts the key handle and encrypted blob from a string previously returned from <see cref="CombineKeyHandleAndPayload"/>.
+ /// </summary>
+ /// <param name="containingMessage">The containing message.</param>
+ /// <param name="messagePart">The message part.</param>
+ /// <param name="keyHandleAndBlob">The value previously returned from <see cref="CombineKeyHandleAndPayload"/>.</param>
+ /// <param name="handle">The crypto key handle.</param>
+ /// <param name="dataBlob">The encrypted/signed data.</param>
+ internal static void ExtractKeyHandleAndPayload(IProtocolMessage containingMessage, string messagePart, string keyHandleAndBlob, out string handle, out string dataBlob) {
+ Requires.NotNull(containingMessage, "containingMessage");
+ Requires.NotNullOrEmpty(messagePart, "messagePart");
+ Requires.NotNullOrEmpty(keyHandleAndBlob, "keyHandleAndBlob");
+
+ int privateHandleIndex = keyHandleAndBlob.IndexOf('!');
+ ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, messagePart, keyHandleAndBlob);
+ handle = keyHandleAndBlob.Substring(0, privateHandleIndex);
+ dataBlob = keyHandleAndBlob.Substring(privateHandleIndex + 1);
+ }
+
+ /// <summary>
+ /// Gets a buffer of random data (not cryptographically strong).
+ /// </summary>
+ /// <param name="length">The length of the sequence to generate.</param>
+ /// <returns>The generated values, which may contain zeros.</returns>
+ internal static byte[] GetNonCryptoRandomData(int length) {
+ byte[] buffer = new byte[length];
+ NonCryptoRandomDataGenerator.NextBytes(buffer);
+ return buffer;
+ }
+
+ /// <summary>
+ /// Gets a cryptographically strong random sequence of values.
+ /// </summary>
+ /// <param name="length">The length of the sequence to generate.</param>
+ /// <returns>The generated values, which may contain zeros.</returns>
+ internal static byte[] GetCryptoRandomData(int length) {
+ byte[] buffer = new byte[length];
+ CryptoRandomDataGenerator.GetBytes(buffer);
+ return buffer;
+ }
+
+ /// <summary>
+ /// Gets a cryptographically strong random sequence of values.
+ /// </summary>
+ /// <param name="binaryLength">The length of the byte sequence to generate.</param>
+ /// <returns>A base64 encoding of the generated random data,
+ /// whose length in characters will likely be greater than <paramref name="binaryLength"/>.</returns>
+ internal static string GetCryptoRandomDataAsBase64(int binaryLength) {
+ byte[] uniq_bytes = GetCryptoRandomData(binaryLength);
+ string uniq = Convert.ToBase64String(uniq_bytes);
+ return uniq;
+ }
+
+ /// <summary>
+ /// Gets a random string made up of a given set of allowable characters.
+ /// </summary>
+ /// <param name="length">The length of the desired random string.</param>
+ /// <param name="allowableCharacters">The allowable characters.</param>
+ /// <returns>A random string.</returns>
+ internal static string GetRandomString(int length, string allowableCharacters) {
+ Requires.InRange(length >= 0, "length");
+ Requires.True(allowableCharacters != null && allowableCharacters.Length >= 2, "allowableCharacters");
+
+ char[] randomString = new char[length];
+ for (int i = 0; i < length; i++) {
+ randomString[i] = allowableCharacters[NonCryptoRandomDataGenerator.Next(allowableCharacters.Length)];
+ }
+
+ return new string(randomString);
+ }
+
+ /// <summary>
+ /// Computes the hash of a string.
+ /// </summary>
+ /// <param name="algorithm">The hash algorithm to use.</param>
+ /// <param name="value">The value to hash.</param>
+ /// <param name="encoding">The encoding to use when converting the string to a byte array.</param>
+ /// <returns>A base64 encoded string.</returns>
+ internal static string ComputeHash(this HashAlgorithm algorithm, string value, Encoding encoding = null) {
+ Requires.NotNull(algorithm, "algorithm");
+ Requires.NotNull(value, "value");
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ encoding = encoding ?? Encoding.UTF8;
+ byte[] bytesToHash = encoding.GetBytes(value);
+ byte[] hash = algorithm.ComputeHash(bytesToHash);
+ string base64Hash = Convert.ToBase64String(hash);
+ return base64Hash;
+ }
+
+ /// <summary>
+ /// Computes the hash of a sequence of key=value pairs.
+ /// </summary>
+ /// <param name="algorithm">The hash algorithm to use.</param>
+ /// <param name="data">The data to hash.</param>
+ /// <param name="encoding">The encoding to use when converting the string to a byte array.</param>
+ /// <returns>A base64 encoded string.</returns>
+ internal static string ComputeHash(this HashAlgorithm algorithm, IDictionary<string, string> data, Encoding encoding = null) {
+ Requires.NotNull(algorithm, "algorithm");
+ Requires.NotNull(data, "data");
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ // Assemble the dictionary to sign, taking care to remove the signature itself
+ // in order to accurately reproduce the original signature (which of course didn't include
+ // the signature).
+ // Also we need to sort the dictionary's keys so that we sign in the same order as we did
+ // the last time.
+ var sortedData = new SortedDictionary<string, string>(data, StringComparer.OrdinalIgnoreCase);
+ return ComputeHash(algorithm, (IEnumerable<KeyValuePair<string, string>>)sortedData, encoding);
+ }
+
+ /// <summary>
+ /// Computes the hash of a sequence of key=value pairs.
+ /// </summary>
+ /// <param name="algorithm">The hash algorithm to use.</param>
+ /// <param name="sortedData">The data to hash.</param>
+ /// <param name="encoding">The encoding to use when converting the string to a byte array.</param>
+ /// <returns>A base64 encoded string.</returns>
+ internal static string ComputeHash(this HashAlgorithm algorithm, IEnumerable<KeyValuePair<string, string>> sortedData, Encoding encoding = null) {
+ Requires.NotNull(algorithm, "algorithm");
+ Requires.NotNull(sortedData, "sortedData");
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ return ComputeHash(algorithm, CreateQueryString(sortedData), encoding);
+ }
+
+ /// <summary>
+ /// Encrypts a byte buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer to encrypt.</param>
+ /// <param name="key">The symmetric secret to use to encrypt the buffer. Allowed values are 128, 192, or 256 bytes in length.</param>
+ /// <returns>The encrypted buffer</returns>
+ internal static byte[] Encrypt(byte[] buffer, byte[] key) {
+ using (SymmetricAlgorithm crypto = CreateSymmetricAlgorithm(key)) {
+ using (var ms = new MemoryStream()) {
+ var binaryWriter = new BinaryWriter(ms);
+ binaryWriter.Write((byte)1); // version of encryption algorithm
+ binaryWriter.Write(crypto.IV);
+ binaryWriter.Flush();
+
+ var cryptoStream = new CryptoStream(ms, crypto.CreateEncryptor(), CryptoStreamMode.Write);
+ cryptoStream.Write(buffer, 0, buffer.Length);
+ cryptoStream.FlushFinalBlock();
+
+ return ms.ToArray();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Decrypts a byte buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer to decrypt.</param>
+ /// <param name="key">The symmetric secret to use to decrypt the buffer. Allowed values are 128, 192, and 256.</param>
+ /// <returns>The encrypted buffer</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")]
+ internal static byte[] Decrypt(byte[] buffer, byte[] key) {
+ using (SymmetricAlgorithm crypto = CreateSymmetricAlgorithm(key)) {
+ using (var ms = new MemoryStream(buffer)) {
+ var binaryReader = new BinaryReader(ms);
+ int algorithmVersion = binaryReader.ReadByte();
+ ErrorUtilities.VerifyProtocol(algorithmVersion == 1, MessagingStrings.UnsupportedEncryptionAlgorithm);
+ crypto.IV = binaryReader.ReadBytes(crypto.IV.Length);
+
+ // Allocate space for the decrypted buffer. We don't know how long it will be yet,
+ // but it will never be larger than the encrypted buffer.
+ var decryptedBuffer = new byte[buffer.Length];
+ int actualDecryptedLength;
+
+ using (var cryptoStream = new CryptoStream(ms, crypto.CreateDecryptor(), CryptoStreamMode.Read)) {
+ actualDecryptedLength = cryptoStream.Read(decryptedBuffer, 0, decryptedBuffer.Length);
+ }
+
+ // Create a new buffer with only the decrypted data.
+ var finalDecryptedBuffer = new byte[actualDecryptedLength];
+ Array.Copy(decryptedBuffer, finalDecryptedBuffer, actualDecryptedLength);
+ return finalDecryptedBuffer;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Encrypts a string.
+ /// </summary>
+ /// <param name="plainText">The text to encrypt.</param>
+ /// <param name="key">The symmetric secret to use to encrypt the buffer. Allowed values are 128, 192, and 256.</param>
+ /// <returns>The encrypted buffer</returns>
+ internal static string Encrypt(string plainText, byte[] key) {
+ byte[] buffer = Encoding.UTF8.GetBytes(plainText);
+ byte[] cipher = Encrypt(buffer, key);
+ return Convert.ToBase64String(cipher);
+ }
+
+ /// <summary>
+ /// Decrypts a string previously encrypted with <see cref="Encrypt(string, byte[])"/>.
+ /// </summary>
+ /// <param name="cipherText">The text to decrypt.</param>
+ /// <param name="key">The symmetric secret to use to decrypt the buffer. Allowed values are 128, 192, and 256.</param>
+ /// <returns>The encrypted buffer</returns>
+ internal static string Decrypt(string cipherText, byte[] key) {
+ byte[] cipher = Convert.FromBase64String(cipherText);
+ byte[] plainText = Decrypt(cipher, key);
+ return Encoding.UTF8.GetString(plainText);
+ }
+
+ /// <summary>
+ /// Performs asymmetric encryption of a given buffer.
+ /// </summary>
+ /// <param name="crypto">The asymmetric encryption provider to use for encryption.</param>
+ /// <param name="buffer">The buffer to encrypt.</param>
+ /// <returns>The encrypted data.</returns>
+ internal static byte[] EncryptWithRandomSymmetricKey(this RSACryptoServiceProvider crypto, byte[] buffer) {
+ Requires.NotNull(crypto, "crypto");
+ Requires.NotNull(buffer, "buffer");
+
+ using (var symmetricCrypto = new RijndaelManaged()) {
+ symmetricCrypto.Mode = CipherMode.CBC;
+
+ using (var encryptedStream = new MemoryStream()) {
+ var encryptedStreamWriter = new BinaryWriter(encryptedStream);
+
+ byte[] prequel = new byte[symmetricCrypto.Key.Length + symmetricCrypto.IV.Length];
+ Array.Copy(symmetricCrypto.Key, prequel, symmetricCrypto.Key.Length);
+ Array.Copy(symmetricCrypto.IV, 0, prequel, symmetricCrypto.Key.Length, symmetricCrypto.IV.Length);
+ byte[] encryptedPrequel = crypto.Encrypt(prequel, false);
+
+ encryptedStreamWriter.Write(encryptedPrequel.Length);
+ encryptedStreamWriter.Write(encryptedPrequel);
+ encryptedStreamWriter.Flush();
+
+ var cryptoStream = new CryptoStream(encryptedStream, symmetricCrypto.CreateEncryptor(), CryptoStreamMode.Write);
+ cryptoStream.Write(buffer, 0, buffer.Length);
+ cryptoStream.FlushFinalBlock();
+
+ return encryptedStream.ToArray();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs asymmetric decryption of a given buffer.
+ /// </summary>
+ /// <param name="crypto">The asymmetric encryption provider to use for decryption.</param>
+ /// <param name="buffer">The buffer to decrypt.</param>
+ /// <returns>The decrypted data.</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")]
+ internal static byte[] DecryptWithRandomSymmetricKey(this RSACryptoServiceProvider crypto, byte[] buffer) {
+ Requires.NotNull(crypto, "crypto");
+ Requires.NotNull(buffer, "buffer");
+
+ using (var encryptedStream = new MemoryStream(buffer)) {
+ var encryptedStreamReader = new BinaryReader(encryptedStream);
+
+ byte[] encryptedPrequel = encryptedStreamReader.ReadBytes(encryptedStreamReader.ReadInt32());
+ byte[] prequel = crypto.Decrypt(encryptedPrequel, false);
+
+ using (var symmetricCrypto = new RijndaelManaged()) {
+ symmetricCrypto.Mode = CipherMode.CBC;
+
+ byte[] symmetricKey = new byte[symmetricCrypto.Key.Length];
+ byte[] symmetricIV = new byte[symmetricCrypto.IV.Length];
+ Array.Copy(prequel, symmetricKey, symmetricKey.Length);
+ Array.Copy(prequel, symmetricKey.Length, symmetricIV, 0, symmetricIV.Length);
+ symmetricCrypto.Key = symmetricKey;
+ symmetricCrypto.IV = symmetricIV;
+
+ // Allocate space for the decrypted buffer. We don't know how long it will be yet,
+ // but it will never be larger than the encrypted buffer.
+ var decryptedBuffer = new byte[encryptedStream.Length - encryptedStream.Position];
+ int actualDecryptedLength;
+
+ using (var cryptoStream = new CryptoStream(encryptedStream, symmetricCrypto.CreateDecryptor(), CryptoStreamMode.Read)) {
+ actualDecryptedLength = cryptoStream.Read(decryptedBuffer, 0, decryptedBuffer.Length);
+ }
+
+ // Create a new buffer with only the decrypted data.
+ var finalDecryptedBuffer = new byte[actualDecryptedLength];
+ Array.Copy(decryptedBuffer, finalDecryptedBuffer, actualDecryptedLength);
+ return finalDecryptedBuffer;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a key from a given bucket with the longest remaining life, or creates a new one if necessary.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ /// <param name="bucket">The bucket where the key should be found or stored.</param>
+ /// <param name="minimumRemainingLife">The minimum remaining life required on the returned key.</param>
+ /// <param name="keySize">The required size of the key, in bits.</param>
+ /// <returns>
+ /// A key-value pair whose key is the secret's handle and whose value is the cryptographic key.
+ /// </returns>
+ internal static KeyValuePair<string, CryptoKey> GetCurrentKey(this ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan minimumRemainingLife, int keySize = 256) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+ Requires.NotNullOrEmpty(bucket, "bucket");
+ Requires.True(keySize % 8 == 0, "keySize");
+
+ var cryptoKeyPair = cryptoKeyStore.GetKeys(bucket).FirstOrDefault(pair => pair.Value.Key.Length == keySize / 8);
+ if (cryptoKeyPair.Value == null || cryptoKeyPair.Value.ExpiresUtc < DateTime.UtcNow + minimumRemainingLife) {
+ // No key exists with enough remaining life for the required purpose. Create a new key.
+ ErrorUtilities.VerifyHost(minimumRemainingLife <= SymmetricSecretKeyLifespan, "Unable to create a new symmetric key with the required lifespan of {0} because it is beyond the limit of {1}.", minimumRemainingLife, SymmetricSecretKeyLifespan);
+ byte[] secret = GetCryptoRandomData(keySize / 8);
+ DateTime expires = DateTime.UtcNow + SymmetricSecretKeyLifespan;
+ var cryptoKey = new CryptoKey(secret, expires);
+
+ // Store this key so we can find and use it later.
+ int failedAttempts = 0;
+ tryAgain:
+ try {
+ string handle = GetRandomString(SymmetricSecretHandleLength, Base64WebSafeCharacters);
+ cryptoKeyPair = new KeyValuePair<string, CryptoKey>(handle, cryptoKey);
+ cryptoKeyStore.StoreKey(bucket, handle, cryptoKey);
+ } catch (CryptoKeyCollisionException) {
+ ErrorUtilities.VerifyInternal(++failedAttempts < 3, "Unable to derive a unique handle to a private symmetric key.");
+ goto tryAgain;
+ }
+ }
+
+ return cryptoKeyPair;
+ }
+
+ /// <summary>
+ /// Compresses a given buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer to compress.</param>
+ /// <returns>The compressed data.</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "This Dispose is safe.")]
+ internal static byte[] Compress(byte[] buffer) {
+ Requires.NotNull(buffer, "buffer");
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+
+ using (var ms = new MemoryStream()) {
+ using (var compressingStream = new DeflateStream(ms, CompressionMode.Compress, true)) {
+ compressingStream.Write(buffer, 0, buffer.Length);
+ }
+
+ return ms.ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Decompresses a given buffer.
+ /// </summary>
+ /// <param name="buffer">The buffer to decompress.</param>
+ /// <returns>The decompressed data.</returns>
+ internal static byte[] Decompress(byte[] buffer) {
+ Requires.NotNull(buffer, "buffer");
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+
+ using (var compressedDataStream = new MemoryStream(buffer)) {
+ using (var decompressedDataStream = new MemoryStream()) {
+ using (var decompressingStream = new DeflateStream(compressedDataStream, CompressionMode.Decompress, true)) {
+ decompressingStream.CopyTo(decompressedDataStream);
+ }
+
+ return decompressedDataStream.ToArray();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converts to data buffer to a base64-encoded string, using web safe characters and with the padding removed.
+ /// </summary>
+ /// <param name="data">The data buffer.</param>
+ /// <returns>A web-safe base64-encoded string without padding.</returns>
+ internal static string ConvertToBase64WebSafeString(byte[] data) {
+ var builder = new StringBuilder(Convert.ToBase64String(data));
+
+ // Swap out the URL-unsafe characters, and trim the padding characters.
+ builder.Replace('+', '-').Replace('/', '_');
+ while (builder[builder.Length - 1] == '=') { // should happen at most twice.
+ builder.Length -= 1;
+ }
+
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Decodes a (web-safe) base64-string back to its binary buffer form.
+ /// </summary>
+ /// <param name="base64WebSafe">The base64-encoded string. May be web-safe encoded.</param>
+ /// <returns>A data buffer.</returns>
+ internal static byte[] FromBase64WebSafeString(string base64WebSafe) {
+ Requires.NotNullOrEmpty(base64WebSafe, "base64WebSafe");
+ Contract.Ensures(Contract.Result<byte[]>() != null);
+
+ // Restore the padding characters and original URL-unsafe characters.
+ int missingPaddingCharacters;
+ switch (base64WebSafe.Length % 4) {
+ case 3:
+ missingPaddingCharacters = 1;
+ break;
+ case 2:
+ missingPaddingCharacters = 2;
+ break;
+ case 0:
+ missingPaddingCharacters = 0;
+ break;
+ default:
+ throw ErrorUtilities.ThrowInternal("No more than two padding characters should be present for base64.");
+ }
+ var builder = new StringBuilder(base64WebSafe, base64WebSafe.Length + missingPaddingCharacters);
+ builder.Replace('-', '+').Replace('_', '/');
+ builder.Append('=', missingPaddingCharacters);
+
+ return Convert.FromBase64String(builder.ToString());
+ }
+
+ /// <summary>
+ /// Compares to string values for ordinal equality in such a way that its execution time does not depend on how much of the value matches.
+ /// </summary>
+ /// <param name="value1">The first value.</param>
+ /// <param name="value2">The second value.</param>
+ /// <returns>A value indicating whether the two strings share ordinal equality.</returns>
+ /// <remarks>
+ /// In signature equality checks, a difference in execution time based on how many initial characters match MAY
+ /// be used as an attack to figure out the expected signature. It is therefore important to make a signature
+ /// equality check's execution time independent of how many characters match the expected value.
+ /// See http://codahale.com/a-lesson-in-timing-attacks/ for more information.
+ /// </remarks>
+ internal static bool EqualsConstantTime(string value1, string value2) {
+ // If exactly one value is null, they don't match.
+ if (value1 == null ^ value2 == null) {
+ return false;
+ }
+
+ // If both values are null (since if one is at this point then they both are), it's a match.
+ if (value1 == null) {
+ return true;
+ }
+
+ if (value1.Length != value2.Length) {
+ return false;
+ }
+
+ // This looks like a pretty crazy way to compare values, but it provides a constant time equality check,
+ // and is more resistant to compiler optimizations than simply setting a boolean flag and returning the boolean after the loop.
+ int result = 0;
+ for (int i = 0; i < value1.Length; i++) {
+ result |= value1[i] ^ value2[i];
+ }
+
+ return result == 0;
+ }
+
+ /// <summary>
+ /// Adds a set of HTTP headers to an <see cref="HttpResponse"/> instance,
+ /// taking care to set some headers to the appropriate properties of
+ /// <see cref="HttpResponse" />
+ /// </summary>
+ /// <param name="headers">The headers to add.</param>
+ /// <param name="response">The <see cref="HttpResponse"/> instance to set the appropriate values to.</param>
+ internal static void ApplyHeadersToResponse(WebHeaderCollection headers, HttpResponse response) {
+ Requires.NotNull(headers, "headers");
+ Requires.NotNull(response, "response");
+
+ foreach (string headerName in headers) {
+ switch (headerName) {
+ case "Content-Type":
+ response.ContentType = headers[HttpResponseHeader.ContentType];
+ break;
+
+ // Add more special cases here as necessary.
+ default:
+ response.AddHeader(headerName, headers[headerName]);
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Adds a set of HTTP headers to an <see cref="HttpResponse"/> instance,
+ /// taking care to set some headers to the appropriate properties of
+ /// <see cref="HttpResponse" />
+ /// </summary>
+ /// <param name="headers">The headers to add.</param>
+ /// <param name="response">The <see cref="HttpListenerResponse"/> instance to set the appropriate values to.</param>
+ internal static void ApplyHeadersToResponse(WebHeaderCollection headers, HttpListenerResponse response) {
+ Requires.NotNull(headers, "headers");
+ Requires.NotNull(response, "response");
+
+ foreach (string headerName in headers) {
+ switch (headerName) {
+ case "Content-Type":
+ response.ContentType = headers[HttpResponseHeader.ContentType];
+ break;
+
+ // Add more special cases here as necessary.
+ default:
+ response.AddHeader(headerName, headers[headerName]);
+ break;
+ }
+ }
+ }
+
+#if !CLR4
+ /// <summary>
+ /// Copies the contents of one stream to another.
+ /// </summary>
+ /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param>
+ /// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param>
+ /// <returns>The total number of bytes copied.</returns>
+ /// <remarks>
+ /// Copying begins at the streams' current positions.
+ /// The positions are NOT reset after copying is complete.
+ /// </remarks>
+ internal static int CopyTo(this Stream copyFrom, Stream copyTo) {
+ Requires.NotNull(copyFrom, "copyFrom");
+ Requires.NotNull(copyTo, "copyTo");
+ Requires.True(copyFrom.CanRead, "copyFrom", MessagingStrings.StreamUnreadable);
+ Requires.True(copyTo.CanWrite, "copyTo", MessagingStrings.StreamUnwritable);
+ return CopyUpTo(copyFrom, copyTo, int.MaxValue);
+ }
+#endif
+
+ /// <summary>
+ /// Copies the contents of one stream to another.
+ /// </summary>
+ /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param>
+ /// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param>
+ /// <param name="maximumBytesToCopy">The maximum bytes to copy.</param>
+ /// <returns>The total number of bytes copied.</returns>
+ /// <remarks>
+ /// Copying begins at the streams' current positions.
+ /// The positions are NOT reset after copying is complete.
+ /// </remarks>
+ internal static int CopyUpTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) {
+ Requires.NotNull(copyFrom, "copyFrom");
+ Requires.NotNull(copyTo, "copyTo");
+ Requires.True(copyFrom.CanRead, "copyFrom", MessagingStrings.StreamUnreadable);
+ Requires.True(copyTo.CanWrite, "copyTo", MessagingStrings.StreamUnwritable);
+
+ byte[] buffer = new byte[1024];
+ int readBytes;
+ int totalCopiedBytes = 0;
+ while ((readBytes = copyFrom.Read(buffer, 0, Math.Min(1024, maximumBytesToCopy))) > 0) {
+ int writeBytes = Math.Min(maximumBytesToCopy, readBytes);
+ copyTo.Write(buffer, 0, writeBytes);
+ totalCopiedBytes += writeBytes;
+ maximumBytesToCopy -= writeBytes;
+ }
+
+ return totalCopiedBytes;
+ }
+
+ /// <summary>
+ /// Creates a snapshot of some stream so it is seekable, and the original can be closed.
+ /// </summary>
+ /// <param name="copyFrom">The stream to copy bytes from.</param>
+ /// <returns>A seekable stream with the same contents as the original.</returns>
+ internal static Stream CreateSnapshot(this Stream copyFrom) {
+ Requires.NotNull(copyFrom, "copyFrom");
+ Requires.True(copyFrom.CanRead, "copyFrom", MessagingStrings.StreamUnreadable);
+
+ MemoryStream copyTo = new MemoryStream(copyFrom.CanSeek ? (int)copyFrom.Length : 4 * 1024);
+ try {
+ copyFrom.CopyTo(copyTo);
+ copyTo.Position = 0;
+ return copyTo;
+ } catch {
+ copyTo.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Clones an <see cref="HttpWebRequest"/> in order to send it again.
+ /// </summary>
+ /// <param name="request">The request to clone.</param>
+ /// <returns>The newly created instance.</returns>
+ internal static HttpWebRequest Clone(this HttpWebRequest request) {
+ Requires.NotNull(request, "request");
+ Requires.True(request.RequestUri != null, "request");
+ return Clone(request, request.RequestUri);
+ }
+
+ /// <summary>
+ /// Clones an <see cref="HttpWebRequest"/> in order to send it again.
+ /// </summary>
+ /// <param name="request">The request to clone.</param>
+ /// <param name="newRequestUri">The new recipient of the request.</param>
+ /// <returns>The newly created instance.</returns>
+ internal static HttpWebRequest Clone(this HttpWebRequest request, Uri newRequestUri) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(newRequestUri, "newRequestUri");
+
+ var newRequest = (HttpWebRequest)WebRequest.Create(newRequestUri);
+
+ // First copy headers. Only set those that are explicitly set on the original request,
+ // because some properties (like IfModifiedSince) activate special behavior when set,
+ // even when set to their "original" values.
+ foreach (string headerName in request.Headers) {
+ switch (headerName) {
+ case "Accept": newRequest.Accept = request.Accept; break;
+ case "Connection": break; // Keep-Alive controls this
+ case "Content-Length": newRequest.ContentLength = request.ContentLength; break;
+ case "Content-Type": newRequest.ContentType = request.ContentType; break;
+ case "Expect": newRequest.Expect = request.Expect; break;
+ case "Host": break; // implicitly copied as part of the RequestUri
+ case "If-Modified-Since": newRequest.IfModifiedSince = request.IfModifiedSince; break;
+ case "Keep-Alive": newRequest.KeepAlive = request.KeepAlive; break;
+ case "Proxy-Connection": break; // no property equivalent?
+ case "Referer": newRequest.Referer = request.Referer; break;
+ case "Transfer-Encoding": newRequest.TransferEncoding = request.TransferEncoding; break;
+ case "User-Agent": newRequest.UserAgent = request.UserAgent; break;
+ default: newRequest.Headers[headerName] = request.Headers[headerName]; break;
+ }
+ }
+
+ newRequest.AllowAutoRedirect = request.AllowAutoRedirect;
+ newRequest.AllowWriteStreamBuffering = request.AllowWriteStreamBuffering;
+ newRequest.AuthenticationLevel = request.AuthenticationLevel;
+ newRequest.AutomaticDecompression = request.AutomaticDecompression;
+ newRequest.CachePolicy = request.CachePolicy;
+ newRequest.ClientCertificates = request.ClientCertificates;
+ newRequest.ConnectionGroupName = request.ConnectionGroupName;
+ newRequest.ContinueDelegate = request.ContinueDelegate;
+ newRequest.CookieContainer = request.CookieContainer;
+ newRequest.Credentials = request.Credentials;
+ newRequest.ImpersonationLevel = request.ImpersonationLevel;
+ newRequest.MaximumAutomaticRedirections = request.MaximumAutomaticRedirections;
+ newRequest.MaximumResponseHeadersLength = request.MaximumResponseHeadersLength;
+ newRequest.MediaType = request.MediaType;
+ newRequest.Method = request.Method;
+ newRequest.Pipelined = request.Pipelined;
+ newRequest.PreAuthenticate = request.PreAuthenticate;
+ newRequest.ProtocolVersion = request.ProtocolVersion;
+ newRequest.ReadWriteTimeout = request.ReadWriteTimeout;
+ newRequest.SendChunked = request.SendChunked;
+ newRequest.Timeout = request.Timeout;
+ newRequest.UseDefaultCredentials = request.UseDefaultCredentials;
+
+ try {
+ newRequest.Proxy = request.Proxy;
+ newRequest.UnsafeAuthenticatedConnectionSharing = request.UnsafeAuthenticatedConnectionSharing;
+ } catch (SecurityException) {
+ Logger.Messaging.Warn("Unable to clone some HttpWebRequest properties due to partial trust.");
+ }
+
+ return newRequest;
+ }
+
+ /// <summary>
+ /// Tests whether two arrays are equal in contents and ordering.
+ /// </summary>
+ /// <typeparam name="T">The type of elements in the arrays.</typeparam>
+ /// <param name="first">The first array in the comparison. May not be null.</param>
+ /// <param name="second">The second array in the comparison. May not be null.</param>
+ /// <returns>True if the arrays equal; false otherwise.</returns>
+ internal static bool AreEquivalent<T>(T[] first, T[] second) {
+ Requires.NotNull(first, "first");
+ Requires.NotNull(second, "second");
+ if (first.Length != second.Length) {
+ return false;
+ }
+ for (int i = 0; i < first.Length; i++) {
+ if (!first[i].Equals(second[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Tests whether two arrays are equal in contents and ordering,
+ /// guaranteeing roughly equivalent execution time regardless of where a signature mismatch may exist.
+ /// </summary>
+ /// <param name="first">The first array in the comparison. May not be null.</param>
+ /// <param name="second">The second array in the comparison. May not be null.</param>
+ /// <returns>True if the arrays equal; false otherwise.</returns>
+ /// <remarks>
+ /// Guaranteeing equal execution time is useful in mitigating against timing attacks on a signature
+ /// or other secret.
+ /// </remarks>
+ internal static bool AreEquivalentConstantTime(byte[] first, byte[] second) {
+ Requires.NotNull(first, "first");
+ Requires.NotNull(second, "second");
+ if (first.Length != second.Length) {
+ return false;
+ }
+
+ int result = 0;
+ for (int i = 0; i < first.Length; i++) {
+ result |= first[i] ^ second[i];
+ }
+ return result == 0;
+ }
+
+ /// <summary>
+ /// Tests two sequences for same contents and ordering.
+ /// </summary>
+ /// <typeparam name="T">The type of elements in the arrays.</typeparam>
+ /// <param name="sequence1">The first sequence in the comparison. May not be null.</param>
+ /// <param name="sequence2">The second sequence in the comparison. May not be null.</param>
+ /// <returns>True if the arrays equal; false otherwise.</returns>
+ internal static bool AreEquivalent<T>(IEnumerable<T> sequence1, IEnumerable<T> sequence2) {
+ if (sequence1 == null && sequence2 == null) {
+ return true;
+ }
+ if ((sequence1 == null) ^ (sequence2 == null)) {
+ return false;
+ }
+
+ IEnumerator<T> iterator1 = sequence1.GetEnumerator();
+ IEnumerator<T> iterator2 = sequence2.GetEnumerator();
+ bool movenext1, movenext2;
+ while (true) {
+ movenext1 = iterator1.MoveNext();
+ movenext2 = iterator2.MoveNext();
+ if (!movenext1 || !movenext2) { // if we've reached the end of at least one sequence
+ break;
+ }
+ object obj1 = iterator1.Current;
+ object obj2 = iterator2.Current;
+ if (obj1 == null && obj2 == null) {
+ continue; // both null is ok
+ }
+ if (obj1 == null ^ obj2 == null) {
+ return false; // exactly one null is different
+ }
+ if (!obj1.Equals(obj2)) {
+ return false; // if they're not equal to each other
+ }
+ }
+
+ return movenext1 == movenext2; // did they both reach the end together?
+ }
+
+ /// <summary>
+ /// Tests two unordered collections for same contents.
+ /// </summary>
+ /// <typeparam name="T">The type of elements in the collections.</typeparam>
+ /// <param name="first">The first collection in the comparison. May not be null.</param>
+ /// <param name="second">The second collection in the comparison. May not be null.</param>
+ /// <returns>True if the collections have the same contents; false otherwise.</returns>
+ internal static bool AreEquivalentUnordered<T>(ICollection<T> first, ICollection<T> second) {
+ if (first == null && second == null) {
+ return true;
+ }
+ if ((first == null) ^ (second == null)) {
+ return false;
+ }
+
+ if (first.Count != second.Count) {
+ return false;
+ }
+
+ foreach (T value in first) {
+ if (!second.Contains(value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Tests whether two dictionaries are equal in length and contents.
+ /// </summary>
+ /// <typeparam name="TKey">The type of keys in the dictionaries.</typeparam>
+ /// <typeparam name="TValue">The type of values in the dictionaries.</typeparam>
+ /// <param name="first">The first dictionary in the comparison. May not be null.</param>
+ /// <param name="second">The second dictionary in the comparison. May not be null.</param>
+ /// <returns>True if the arrays equal; false otherwise.</returns>
+ internal static bool AreEquivalent<TKey, TValue>(IDictionary<TKey, TValue> first, IDictionary<TKey, TValue> second) {
+ Requires.NotNull(first, "first");
+ Requires.NotNull(second, "second");
+ return AreEquivalent(first.ToArray(), second.ToArray());
+ }
+
+ /// <summary>
+ /// Concatenates a list of name-value pairs as key=value&amp;key=value,
+ /// taking care to properly encode each key and value for URL
+ /// transmission according to RFC 3986. No ? is prefixed to the string.
+ /// </summary>
+ /// <param name="args">The dictionary of key/values to read from.</param>
+ /// <returns>The formulated querystring style string.</returns>
+ internal static string CreateQueryString(IEnumerable<KeyValuePair<string, string>> args) {
+ Requires.NotNull(args, "args");
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ if (args.Count() == 0) {
+ return string.Empty;
+ }
+ StringBuilder sb = new StringBuilder(args.Count() * 10);
+
+ foreach (var p in args) {
+ ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(p.Key), MessagingStrings.UnexpectedNullOrEmptyKey);
+ ErrorUtilities.VerifyArgument(p.Value != null, MessagingStrings.UnexpectedNullValue, p.Key);
+ sb.Append(EscapeUriDataStringRfc3986(p.Key));
+ sb.Append('=');
+ sb.Append(EscapeUriDataStringRfc3986(p.Value));
+ sb.Append('&');
+ }
+ sb.Length--; // remove trailing &
+
+ return sb.ToString();
+ }
+
+ /// <summary>
+ /// Adds a set of name-value pairs to the end of a given URL
+ /// as part of the querystring piece. Prefixes a ? or &amp; before
+ /// first element as necessary.
+ /// </summary>
+ /// <param name="builder">The UriBuilder to add arguments to.</param>
+ /// <param name="args">
+ /// The arguments to add to the query.
+ /// If null, <paramref name="builder"/> is not changed.
+ /// </param>
+ /// <remarks>
+ /// If the parameters to add match names of parameters that already are defined
+ /// in the query string, the existing ones are <i>not</i> replaced.
+ /// </remarks>
+ internal static void AppendQueryArgs(this UriBuilder builder, IEnumerable<KeyValuePair<string, string>> args) {
+ Requires.NotNull(builder, "builder");
+
+ if (args != null && args.Count() > 0) {
+ StringBuilder sb = new StringBuilder(50 + (args.Count() * 10));
+ if (!string.IsNullOrEmpty(builder.Query)) {
+ sb.Append(builder.Query.Substring(1));
+ sb.Append('&');
+ }
+ sb.Append(CreateQueryString(args));
+
+ builder.Query = sb.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Adds a set of name-value pairs to the end of a given URL
+ /// as part of the fragment piece. Prefixes a # or &amp; before
+ /// first element as necessary.
+ /// </summary>
+ /// <param name="builder">The UriBuilder to add arguments to.</param>
+ /// <param name="args">
+ /// The arguments to add to the query.
+ /// If null, <paramref name="builder"/> is not changed.
+ /// </param>
+ /// <remarks>
+ /// If the parameters to add match names of parameters that already are defined
+ /// in the fragment, the existing ones are <i>not</i> replaced.
+ /// </remarks>
+ internal static void AppendFragmentArgs(this UriBuilder builder, IEnumerable<KeyValuePair<string, string>> args) {
+ Requires.NotNull(builder, "builder");
+
+ if (args != null && args.Count() > 0) {
+ StringBuilder sb = new StringBuilder(50 + (args.Count() * 10));
+ if (!string.IsNullOrEmpty(builder.Fragment)) {
+ sb.Append(builder.Fragment);
+ sb.Append('&');
+ }
+ sb.Append(CreateQueryString(args));
+
+ builder.Fragment = sb.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Adds parameters to a query string, replacing parameters that
+ /// match ones that already exist in the query string.
+ /// </summary>
+ /// <param name="builder">The UriBuilder to add arguments to.</param>
+ /// <param name="args">
+ /// The arguments to add to the query.
+ /// If null, <paramref name="builder"/> is not changed.
+ /// </param>
+ internal static void AppendAndReplaceQueryArgs(this UriBuilder builder, IEnumerable<KeyValuePair<string, string>> args) {
+ Requires.NotNull(builder, "builder");
+
+ if (args != null && args.Count() > 0) {
+ NameValueCollection aggregatedArgs = HttpUtility.ParseQueryString(builder.Query);
+ foreach (var pair in args) {
+ aggregatedArgs[pair.Key] = pair.Value;
+ }
+
+ builder.Query = CreateQueryString(aggregatedArgs.ToDictionary());
+ }
+ }
+
+ /// <summary>
+ /// Extracts the recipient from an HttpRequestInfo.
+ /// </summary>
+ /// <param name="request">The request to get recipient information from.</param>
+ /// <returns>The recipient.</returns>
+ /// <exception cref="ArgumentException">Thrown if the HTTP request is something we can't handle.</exception>
+ internal static MessageReceivingEndpoint GetRecipient(this HttpRequestInfo request) {
+ return new MessageReceivingEndpoint(request.UrlBeforeRewriting, GetHttpDeliveryMethod(request.HttpMethod));
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpDeliveryMethods"/> enum value for a given HTTP verb.
+ /// </summary>
+ /// <param name="httpVerb">The HTTP verb.</param>
+ /// <returns>A <see cref="HttpDeliveryMethods"/> enum value that is within the <see cref="HttpDeliveryMethods.HttpVerbMask"/>.</returns>
+ /// <exception cref="ArgumentException">Thrown if the HTTP request is something we can't handle.</exception>
+ internal static HttpDeliveryMethods GetHttpDeliveryMethod(string httpVerb) {
+ if (httpVerb == "GET") {
+ return HttpDeliveryMethods.GetRequest;
+ } else if (httpVerb == "POST") {
+ return HttpDeliveryMethods.PostRequest;
+ } else if (httpVerb == "PUT") {
+ return HttpDeliveryMethods.PutRequest;
+ } else if (httpVerb == "DELETE") {
+ return HttpDeliveryMethods.DeleteRequest;
+ } else if (httpVerb == "HEAD") {
+ return HttpDeliveryMethods.HeadRequest;
+ } else {
+ throw ErrorUtilities.ThrowArgumentNamed("httpVerb", MessagingStrings.UnsupportedHttpVerb, httpVerb);
+ }
+ }
+
+ /// <summary>
+ /// Gets the HTTP verb to use for a given <see cref="HttpDeliveryMethods"/> enum value.
+ /// </summary>
+ /// <param name="httpMethod">The HTTP method.</param>
+ /// <returns>An HTTP verb, such as GET, POST, PUT, or DELETE.</returns>
+ internal static string GetHttpVerb(HttpDeliveryMethods httpMethod) {
+ if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.GetRequest) {
+ return "GET";
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PostRequest) {
+ return "POST";
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PutRequest) {
+ return "PUT";
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.DeleteRequest) {
+ return "DELETE";
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.HeadRequest) {
+ return "HEAD";
+ } else if ((httpMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) {
+ return "GET"; // if AuthorizationHeaderRequest is specified without an explicit HTTP verb, assume GET.
+ } else {
+ throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod);
+ }
+ }
+
+ /// <summary>
+ /// Copies some extra parameters into a message.
+ /// </summary>
+ /// <param name="messageDictionary">The message to copy the extra data into.</param>
+ /// <param name="extraParameters">The extra data to copy into the message. May be null to do nothing.</param>
+ internal static void AddExtraParameters(this MessageDictionary messageDictionary, IDictionary<string, string> extraParameters) {
+ Requires.NotNull(messageDictionary, "messageDictionary");
+
+ if (extraParameters != null) {
+ foreach (var pair in extraParameters) {
+ try {
+ messageDictionary.Add(pair);
+ } catch (ArgumentException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.ExtraParameterAddFailure, pair.Key, pair.Value);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Collects a sequence of key=value pairs into a dictionary.
+ /// </summary>
+ /// <typeparam name="TKey">The type of the key.</typeparam>
+ /// <typeparam name="TValue">The type of the value.</typeparam>
+ /// <param name="sequence">The sequence.</param>
+ /// <returns>A dictionary.</returns>
+ internal static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> sequence) {
+ Requires.NotNull(sequence, "sequence");
+ return sequence.ToDictionary(pair => pair.Key, pair => pair.Value);
+ }
+
+ /// <summary>
+ /// Converts a <see cref="NameValueCollection"/> to an IDictionary&lt;string, string&gt;.
+ /// </summary>
+ /// <param name="nvc">The NameValueCollection to convert. May be null.</param>
+ /// <returns>The generated dictionary, or null if <paramref name="nvc"/> is null.</returns>
+ /// <remarks>
+ /// If a <c>null</c> key is encountered, its value is ignored since
+ /// <c>Dictionary&lt;string, string&gt;</c> does not allow null keys.
+ /// </remarks>
+ internal static Dictionary<string, string> ToDictionary(this NameValueCollection nvc) {
+ Contract.Ensures((nvc != null && Contract.Result<Dictionary<string, string>>() != null) || (nvc == null && Contract.Result<Dictionary<string, string>>() == null));
+ return ToDictionary(nvc, false);
+ }
+
+ /// <summary>
+ /// Converts a <see cref="NameValueCollection"/> to an IDictionary&lt;string, string&gt;.
+ /// </summary>
+ /// <param name="nvc">The NameValueCollection to convert. May be null.</param>
+ /// <param name="throwOnNullKey">
+ /// A value indicating whether a null key in the <see cref="NameValueCollection"/> should be silently skipped since it is not a valid key in a Dictionary.
+ /// Use <c>true</c> to throw an exception if a null key is encountered.
+ /// Use <c>false</c> to silently continue converting the valid keys.
+ /// </param>
+ /// <returns>The generated dictionary, or null if <paramref name="nvc"/> is null.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="throwOnNullKey"/> is <c>true</c> and a null key is encountered.</exception>
+ internal static Dictionary<string, string> ToDictionary(this NameValueCollection nvc, bool throwOnNullKey) {
+ Contract.Ensures((nvc != null && Contract.Result<Dictionary<string, string>>() != null) || (nvc == null && Contract.Result<Dictionary<string, string>>() == null));
+ if (nvc == null) {
+ return null;
+ }
+
+ var dictionary = new Dictionary<string, string>();
+ foreach (string key in nvc) {
+ // NameValueCollection supports a null key, but Dictionary<K,V> does not.
+ if (key == null) {
+ if (throwOnNullKey) {
+ throw new ArgumentException(MessagingStrings.UnexpectedNullKey);
+ } else {
+ // Only emit a warning if there was a non-empty value.
+ if (!string.IsNullOrEmpty(nvc[key])) {
+ Logger.OpenId.WarnFormat("Null key with value {0} encountered while translating NameValueCollection to Dictionary.", nvc[key]);
+ }
+ }
+ } else {
+ dictionary.Add(key, nvc[key]);
+ }
+ }
+
+ return dictionary;
+ }
+
+ /// <summary>
+ /// Sorts the elements of a sequence in ascending order by using a specified comparer.
+ /// </summary>
+ /// <typeparam name="TSource">The type of the elements of source.</typeparam>
+ /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
+ /// <param name="source">A sequence of values to order.</param>
+ /// <param name="keySelector">A function to extract a key from an element.</param>
+ /// <param name="comparer">A comparison function to compare keys.</param>
+ /// <returns>An System.Linq.IOrderedEnumerable&lt;TElement&gt; whose elements are sorted according to a key.</returns>
+ internal static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Comparison<TKey> comparer) {
+ Requires.NotNull(source, "source");
+ Requires.NotNull(comparer, "comparer");
+ Requires.NotNull(keySelector, "keySelector");
+ Contract.Ensures(Contract.Result<IOrderedEnumerable<TSource>>() != null);
+ return System.Linq.Enumerable.OrderBy<TSource, TKey>(source, keySelector, new ComparisonHelper<TKey>(comparer));
+ }
+
+ /// <summary>
+ /// Determines whether the specified message is a request (indirect message or direct request).
+ /// </summary>
+ /// <param name="message">The message in question.</param>
+ /// <returns>
+ /// <c>true</c> if the specified message is a request; otherwise, <c>false</c>.
+ /// </returns>
+ /// <remarks>
+ /// Although an <see cref="IProtocolMessage"/> may implement the <see cref="IDirectedProtocolMessage"/>
+ /// interface, it may only be doing that for its derived classes. These objects are only requests
+ /// if their <see cref="IDirectedProtocolMessage.Recipient"/> property is non-null.
+ /// </remarks>
+ internal static bool IsRequest(this IDirectedProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ return message.Recipient != null;
+ }
+
+ /// <summary>
+ /// Determines whether the specified message is a direct response.
+ /// </summary>
+ /// <param name="message">The message in question.</param>
+ /// <returns>
+ /// <c>true</c> if the specified message is a direct response; otherwise, <c>false</c>.
+ /// </returns>
+ /// <remarks>
+ /// Although an <see cref="IProtocolMessage"/> may implement the
+ /// <see cref="IDirectResponseProtocolMessage"/> interface, it may only be doing
+ /// that for its derived classes. These objects are only requests if their
+ /// <see cref="IDirectResponseProtocolMessage.OriginatingRequest"/> property is non-null.
+ /// </remarks>
+ internal static bool IsDirectResponse(this IDirectResponseProtocolMessage message) {
+ Requires.NotNull(message, "message");
+ return message.OriginatingRequest != null;
+ }
+
+ /// <summary>
+ /// Writes a buffer, prefixed with its own length.
+ /// </summary>
+ /// <param name="writer">The binary writer.</param>
+ /// <param name="buffer">The buffer.</param>
+ internal static void WriteBuffer(this BinaryWriter writer, byte[] buffer) {
+ Requires.NotNull(writer, "writer");
+ Requires.NotNull(buffer, "buffer");
+ writer.Write(buffer.Length);
+ writer.Write(buffer, 0, buffer.Length);
+ }
+
+ /// <summary>
+ /// Reads a buffer that is prefixed with its own length.
+ /// </summary>
+ /// <param name="reader">The binary reader positioned at the buffer length.</param>
+ /// <returns>The read buffer.</returns>
+ internal static byte[] ReadBuffer(this BinaryReader reader) {
+ Requires.NotNull(reader, "reader");
+ int length = reader.ReadInt32();
+ byte[] buffer = new byte[length];
+ ErrorUtilities.VerifyProtocol(reader.Read(buffer, 0, length) == length, "Unexpected buffer length.");
+ return buffer;
+ }
+
+ /// <summary>
+ /// Constructs a Javascript expression that will create an object
+ /// on the user agent when assigned to a variable.
+ /// </summary>
+ /// <param name="namesAndValues">The untrusted names and untrusted values to inject into the JSON object.</param>
+ /// <param name="valuesPreEncoded">if set to <c>true</c> the values will NOT be escaped as if it were a pure string.</param>
+ /// <returns>The Javascript JSON object as a string.</returns>
+ internal static string CreateJsonObject(IEnumerable<KeyValuePair<string, string>> namesAndValues, bool valuesPreEncoded) {
+ StringBuilder builder = new StringBuilder();
+ builder.Append("{ ");
+
+ foreach (var pair in namesAndValues) {
+ builder.Append(MessagingUtilities.GetSafeJavascriptValue(pair.Key));
+ builder.Append(": ");
+ builder.Append(valuesPreEncoded ? pair.Value : MessagingUtilities.GetSafeJavascriptValue(pair.Value));
+ builder.Append(",");
+ }
+
+ if (builder[builder.Length - 1] == ',') {
+ builder.Length -= 1;
+ }
+ builder.Append("}");
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Prepares what SHOULD be simply a string value for safe injection into Javascript
+ /// by using appropriate character escaping.
+ /// </summary>
+ /// <param name="value">The untrusted string value to be escaped to protected against XSS attacks. May be null.</param>
+ /// <returns>The escaped string, surrounded by single-quotes.</returns>
+ internal static string GetSafeJavascriptValue(string value) {
+ if (value == null) {
+ return "null";
+ }
+
+ // We use a StringBuilder because we have potentially many replacements to do,
+ // and we don't want to create a new string for every intermediate replacement step.
+ StringBuilder builder = new StringBuilder(value);
+ foreach (var pair in javascriptStaticStringEscaping) {
+ builder.Replace(pair.Key, pair.Value);
+ }
+ builder.Insert(0, '\'');
+ builder.Append('\'');
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Escapes a string according to the URI data string rules given in RFC 3986.
+ /// </summary>
+ /// <param name="value">The value to escape.</param>
+ /// <returns>The escaped value.</returns>
+ /// <remarks>
+ /// The <see cref="Uri.EscapeDataString"/> method is <i>supposed</i> to take on
+ /// RFC 3986 behavior if certain elements are present in a .config file. Even if this
+ /// actually worked (which in my experiments it <i>doesn't</i>), we can't rely on every
+ /// host actually having this configuration element present.
+ /// </remarks>
+ internal static string EscapeUriDataStringRfc3986(string value) {
+ Requires.NotNull(value, "value");
+
+ // Start with RFC 2396 escaping by calling the .NET method to do the work.
+ // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation).
+ // If it does, the escaping we do that follows it will be a no-op since the
+ // characters we search for to replace can't possibly exist in the string.
+ StringBuilder escaped = new StringBuilder(Uri.EscapeDataString(value));
+
+ // Upgrade the escaping to RFC 3986, if necessary.
+ for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++) {
+ escaped.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0]));
+ }
+
+ // Return the fully-RFC3986-escaped string.
+ return escaped.ToString();
+ }
+
+ /// <summary>
+ /// Ensures that UTC times are converted to local times. Unspecified kinds are unchanged.
+ /// </summary>
+ /// <param name="value">The date-time to convert.</param>
+ /// <returns>The date-time in local time.</returns>
+ internal static DateTime ToLocalTimeSafe(this DateTime value) {
+ if (value.Kind == DateTimeKind.Unspecified) {
+ return value;
+ }
+
+ return value.ToLocalTime();
+ }
+
+ /// <summary>
+ /// Ensures that local times are converted to UTC times. Unspecified kinds are unchanged.
+ /// </summary>
+ /// <param name="value">The date-time to convert.</param>
+ /// <returns>The date-time in UTC time.</returns>
+ internal static DateTime ToUniversalTimeSafe(this DateTime value) {
+ if (value.Kind == DateTimeKind.Unspecified) {
+ return value;
+ }
+
+ return value.ToUniversalTime();
+ }
+
+ /// <summary>
+ /// Creates a symmetric algorithm for use in encryption/decryption.
+ /// </summary>
+ /// <param name="key">The symmetric key to use for encryption/decryption.</param>
+ /// <returns>A symmetric algorithm.</returns>
+ private static SymmetricAlgorithm CreateSymmetricAlgorithm(byte[] key) {
+ SymmetricAlgorithm result = null;
+ try {
+ result = new RijndaelManaged();
+ result.Mode = CipherMode.CBC;
+ result.Key = key;
+ return result;
+ } catch {
+ IDisposable disposableResult = result;
+ if (disposableResult != null) {
+ disposableResult.Dispose();
+ }
+
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// A class to convert a <see cref="Comparison&lt;T&gt;"/> into an <see cref="IComparer&lt;T&gt;"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of objects being compared.</typeparam>
+ private class ComparisonHelper<T> : IComparer<T> {
+ /// <summary>
+ /// The comparison method to use.
+ /// </summary>
+ private Comparison<T> comparison;
+
+ /// <summary>
+ /// Initializes a new instance of the ComparisonHelper class.
+ /// </summary>
+ /// <param name="comparison">The comparison method to use.</param>
+ internal ComparisonHelper(Comparison<T> comparison) {
+ Requires.NotNull(comparison, "comparison");
+
+ this.comparison = comparison;
+ }
+
+ #region IComparer<T> Members
+
+ /// <summary>
+ /// Compares two instances of <typeparamref name="T"/>.
+ /// </summary>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ /// <returns>Any of -1, 0, or 1 according to standard comparison rules.</returns>
+ public int Compare(T x, T y) {
+ return this.comparison(x, y);
+ }
+
+ #endregion
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs b/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs
new file mode 100644
index 0000000..f72ad6c
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/MultipartPostPart.cs
@@ -0,0 +1,223 @@
+//-----------------------------------------------------------------------
+// <copyright file="MultipartPostPart.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using System.Text;
+
+ /// <summary>
+ /// Represents a single part in a HTTP multipart POST request.
+ /// </summary>
+ public class MultipartPostPart : IDisposable {
+ /// <summary>
+ /// The "Content-Disposition" string.
+ /// </summary>
+ private const string ContentDispositionHeader = "Content-Disposition";
+
+ /// <summary>
+ /// The two-character \r\n newline character sequence to use.
+ /// </summary>
+ private const string NewLine = "\r\n";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartPostPart"/> class.
+ /// </summary>
+ /// <param name="contentDisposition">The content disposition of the part.</param>
+ public MultipartPostPart(string contentDisposition) {
+ Requires.NotNullOrEmpty(contentDisposition, "contentDisposition");
+
+ this.ContentDisposition = contentDisposition;
+ this.ContentAttributes = new Dictionary<string, string>();
+ this.PartHeaders = new WebHeaderCollection();
+ }
+
+ /// <summary>
+ /// Gets or sets the content disposition.
+ /// </summary>
+ /// <value>The content disposition.</value>
+ public string ContentDisposition { get; set; }
+
+ /// <summary>
+ /// Gets the key=value attributes that appear on the same line as the Content-Disposition.
+ /// </summary>
+ /// <value>The content attributes.</value>
+ public IDictionary<string, string> ContentAttributes { get; private set; }
+
+ /// <summary>
+ /// Gets the headers that appear on subsequent lines after the Content-Disposition.
+ /// </summary>
+ public WebHeaderCollection PartHeaders { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the content of the part.
+ /// </summary>
+ public Stream Content { get; set; }
+
+ /// <summary>
+ /// Gets the length of this entire part.
+ /// </summary>
+ /// <remarks>Useful for calculating the ContentLength HTTP header to send before actually serializing the content.</remarks>
+ public long Length {
+ get {
+ ErrorUtilities.VerifyOperation(this.Content != null && this.Content.Length >= 0, MessagingStrings.StreamMustHaveKnownLength);
+
+ long length = 0;
+ length += ContentDispositionHeader.Length;
+ length += ": ".Length;
+ length += this.ContentDisposition.Length;
+ foreach (var pair in this.ContentAttributes) {
+ length += "; ".Length + pair.Key.Length + "=\"".Length + pair.Value.Length + "\"".Length;
+ }
+
+ length += NewLine.Length;
+ foreach (string headerName in this.PartHeaders) {
+ length += headerName.Length;
+ length += ": ".Length;
+ length += this.PartHeaders[headerName].Length;
+ length += NewLine.Length;
+ }
+
+ length += NewLine.Length;
+ length += this.Content.Length;
+
+ return length;
+ }
+ }
+
+ /// <summary>
+ /// Creates a part that represents a simple form field.
+ /// </summary>
+ /// <param name="name">The name of the form field.</param>
+ /// <param name="value">The value.</param>
+ /// <returns>The constructed part.</returns>
+ public static MultipartPostPart CreateFormPart(string name, string value) {
+ Requires.NotNullOrEmpty(name, "name");
+ Requires.NotNull(value, "value");
+
+ var part = new MultipartPostPart("form-data");
+ try {
+ part.ContentAttributes["name"] = name;
+ part.Content = new MemoryStream(Encoding.UTF8.GetBytes(value));
+ return part;
+ } catch {
+ part.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Creates a part that represents a file attachment.
+ /// </summary>
+ /// <param name="name">The name of the form field.</param>
+ /// <param name="filePath">The path to the file to send.</param>
+ /// <param name="contentType">Type of the content in HTTP Content-Type format.</param>
+ /// <returns>The constructed part.</returns>
+ public static MultipartPostPart CreateFormFilePart(string name, string filePath, string contentType) {
+ Requires.NotNullOrEmpty(name, "name");
+ Requires.NotNullOrEmpty(filePath, "filePath");
+ Requires.NotNullOrEmpty(contentType, "contentType");
+
+ string fileName = Path.GetFileName(filePath);
+ var fileStream = File.OpenRead(filePath);
+ try {
+ return CreateFormFilePart(name, fileName, contentType, fileStream);
+ } catch {
+ fileStream.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Creates a part that represents a file attachment.
+ /// </summary>
+ /// <param name="name">The name of the form field.</param>
+ /// <param name="fileName">Name of the file as the server should see it.</param>
+ /// <param name="contentType">Type of the content in HTTP Content-Type format.</param>
+ /// <param name="content">The content of the file.</param>
+ /// <returns>The constructed part.</returns>
+ public static MultipartPostPart CreateFormFilePart(string name, string fileName, string contentType, Stream content) {
+ Requires.NotNullOrEmpty(name, "name");
+ Requires.NotNullOrEmpty(fileName, "fileName");
+ Requires.NotNullOrEmpty(contentType, "contentType");
+ Requires.NotNull(content, "content");
+
+ var part = new MultipartPostPart("file");
+ try {
+ part.ContentAttributes["name"] = name;
+ part.ContentAttributes["filename"] = fileName;
+ part.PartHeaders[HttpRequestHeader.ContentType] = contentType;
+ if (!contentType.StartsWith("text/", StringComparison.Ordinal)) {
+ part.PartHeaders["Content-Transfer-Encoding"] = "binary";
+ }
+
+ part.Content = content;
+ return part;
+ } catch {
+ part.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Serializes the part to a stream.
+ /// </summary>
+ /// <param name="streamWriter">The stream writer.</param>
+ internal void Serialize(StreamWriter streamWriter) {
+ // VERY IMPORTANT: any changes at all made to this must be kept in sync with the
+ // Length property which calculates exactly how many bytes this method will write.
+ streamWriter.NewLine = NewLine;
+ streamWriter.Write("{0}: {1}", ContentDispositionHeader, this.ContentDisposition);
+ foreach (var pair in this.ContentAttributes) {
+ streamWriter.Write("; {0}=\"{1}\"", pair.Key, pair.Value);
+ }
+
+ streamWriter.WriteLine();
+ foreach (string headerName in this.PartHeaders) {
+ streamWriter.WriteLine("{0}: {1}", headerName, this.PartHeaders[headerName]);
+ }
+
+ streamWriter.WriteLine();
+ streamWriter.Flush();
+ this.Content.CopyTo(streamWriter.BaseStream);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ this.Content.Dispose();
+ }
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void Invariant() {
+ Contract.Invariant(!string.IsNullOrEmpty(this.ContentDisposition));
+ Contract.Invariant(this.PartHeaders != null);
+ Contract.Invariant(this.ContentAttributes != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs
new file mode 100644
index 0000000..8fb69a1
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/NetworkDirectWebResponse.cs
@@ -0,0 +1,116 @@
+//-----------------------------------------------------------------------
+// <copyright file="NetworkDirectWebResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using System.Text;
+
+ /// <summary>
+ /// A live network HTTP response
+ /// </summary>
+ [DebuggerDisplay("{Status} {ContentType.MediaType}")]
+ [ContractVerification(true)]
+ internal class NetworkDirectWebResponse : IncomingWebResponse, IDisposable {
+ /// <summary>
+ /// The network response object, used to initialize this instance, that still needs
+ /// to be closed if applicable.
+ /// </summary>
+ private HttpWebResponse httpWebResponse;
+
+ /// <summary>
+ /// The incoming network response stream.
+ /// </summary>
+ private Stream responseStream;
+
+ /// <summary>
+ /// A value indicating whether a stream reader has already been
+ /// created on this instance.
+ /// </summary>
+ private bool streamReadBegun;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NetworkDirectWebResponse"/> class.
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ /// <param name="response">The response.</param>
+ internal NetworkDirectWebResponse(Uri requestUri, HttpWebResponse response)
+ : base(requestUri, response) {
+ Requires.NotNull(requestUri, "requestUri");
+ Requires.NotNull(response, "response");
+ this.httpWebResponse = response;
+ this.responseStream = response.GetResponseStream();
+ }
+
+ /// <summary>
+ /// Gets the body of the HTTP response.
+ /// </summary>
+ public override Stream ResponseStream {
+ get { return this.responseStream; }
+ }
+
+ /// <summary>
+ /// Creates a text reader for the response stream.
+ /// </summary>
+ /// <returns>The text reader, initialized for the proper encoding.</returns>
+ public override StreamReader GetResponseReader() {
+ this.streamReadBegun = true;
+ if (this.responseStream == null) {
+ throw new ObjectDisposedException(GetType().Name);
+ }
+
+ string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding];
+ if (string.IsNullOrEmpty(contentEncoding)) {
+ return new StreamReader(this.ResponseStream);
+ } else {
+ return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding));
+ }
+ }
+
+ /// <summary>
+ /// Gets an offline snapshot version of this instance.
+ /// </summary>
+ /// <param name="maximumBytesToCache">The maximum bytes from the response stream to cache.</param>
+ /// <returns>A snapshot version of this instance.</returns>
+ /// <remarks>
+ /// If this instance is a <see cref="NetworkDirectWebResponse"/> creating a snapshot
+ /// will automatically close and dispose of the underlying response stream.
+ /// If this instance is a <see cref="CachedDirectWebResponse"/>, the result will
+ /// be the self same instance.
+ /// </remarks>
+ internal override CachedDirectWebResponse GetSnapshot(int maximumBytesToCache) {
+ ErrorUtilities.VerifyOperation(!this.streamReadBegun, "Network stream reading has already begun.");
+ ErrorUtilities.VerifyOperation(this.httpWebResponse != null, "httpWebResponse != null");
+
+ this.streamReadBegun = true;
+ var result = new CachedDirectWebResponse(this.RequestUri, this.httpWebResponse, maximumBytesToCache);
+ this.Dispose();
+ return result;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool disposing) {
+ if (disposing) {
+ if (this.responseStream != null) {
+ this.responseStream.Dispose();
+ this.responseStream = null;
+ }
+ if (this.httpWebResponse != null) {
+ this.httpWebResponse.Close();
+ this.httpWebResponse = null;
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs
new file mode 100644
index 0000000..003cac8
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs
@@ -0,0 +1,300 @@
+//-----------------------------------------------------------------------
+// <copyright file="OutgoingWebResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Threading;
+ using System.Web;
+
+ /// <summary>
+ /// A protocol message (request or response) that passes from this
+ /// to a remote party via the user agent using a redirect or form
+ /// POST submission, OR a direct message response.
+ /// </summary>
+ /// <remarks>
+ /// <para>An instance of this type describes the HTTP response that must be sent
+ /// in response to the current HTTP request.</para>
+ /// <para>It is important that this response make up the entire HTTP response.
+ /// A hosting ASPX page should not be allowed to render its normal HTML output
+ /// after this response is sent. The normal rendered output of an ASPX page
+ /// can be canceled by calling <see cref="HttpResponse.End"/> after this message
+ /// is sent on the response stream.</para>
+ /// </remarks>
+ public class OutgoingWebResponse {
+ /// <summary>
+ /// The encoder to use for serializing the response body.
+ /// </summary>
+ private static Encoding bodyStringEncoder = new UTF8Encoding(false);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutgoingWebResponse"/> class.
+ /// </summary>
+ internal OutgoingWebResponse() {
+ this.Status = HttpStatusCode.OK;
+ this.Headers = new WebHeaderCollection();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutgoingWebResponse"/> class
+ /// based on the contents of an <see cref="HttpWebResponse"/>.
+ /// </summary>
+ /// <param name="response">The <see cref="HttpWebResponse"/> to clone.</param>
+ /// <param name="maximumBytesToRead">The maximum bytes to read from the response stream.</param>
+ protected internal OutgoingWebResponse(HttpWebResponse response, int maximumBytesToRead) {
+ Requires.NotNull(response, "response");
+
+ this.Status = response.StatusCode;
+ this.Headers = response.Headers;
+ this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength);
+ using (Stream responseStream = response.GetResponseStream()) {
+ // BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here.
+ this.IsResponseTruncated = responseStream.CopyUpTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead;
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ }
+ }
+
+ /// <summary>
+ /// Gets the headers that must be included in the response to the user agent.
+ /// </summary>
+ /// <remarks>
+ /// The headers in this collection are not meant to be a comprehensive list
+ /// of exactly what should be sent, but are meant to augment whatever headers
+ /// are generally included in a typical response.
+ /// </remarks>
+ public WebHeaderCollection Headers { get; internal set; }
+
+ /// <summary>
+ /// Gets the body of the HTTP response.
+ /// </summary>
+ public Stream ResponseStream { get; internal set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the response stream is incomplete due
+ /// to a length limitation imposed by the HttpWebRequest or calling method.
+ /// </summary>
+ public bool IsResponseTruncated { get; internal set; }
+
+ /// <summary>
+ /// Gets or sets the body of the response as a string.
+ /// </summary>
+ public string Body {
+ get { return this.ResponseStream != null ? this.GetResponseReader().ReadToEnd() : null; }
+ set { this.SetResponse(value, null); }
+ }
+
+ /// <summary>
+ /// Gets the HTTP status code to use in the HTTP response.
+ /// </summary>
+ public HttpStatusCode Status { get; internal set; }
+
+ /// <summary>
+ /// Gets or sets a reference to the actual protocol message that
+ /// is being sent via the user agent.
+ /// </summary>
+ internal IProtocolMessage OriginalMessage { get; set; }
+
+ /// <summary>
+ /// Creates a text reader for the response stream.
+ /// </summary>
+ /// <returns>The text reader, initialized for the proper encoding.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Costly operation")]
+ public StreamReader GetResponseReader() {
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding];
+ if (string.IsNullOrEmpty(contentEncoding)) {
+ return new StreamReader(this.ResponseStream);
+ } else {
+ return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding));
+ }
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent
+ /// and ends execution on the current page or handler.
+ /// </summary>
+ /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception>
+ /// <remarks>
+ /// Requires a current HttpContext.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual void Send() {
+ Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+
+ this.Send(HttpContext.Current);
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent
+ /// and ends execution on the current page or handler.
+ /// </summary>
+ /// <param name="context">The context of the HTTP request whose response should be set.
+ /// Typically this is <see cref="HttpContext.Current"/>.</param>
+ /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual void Send(HttpContext context) {
+ this.Respond(context, true);
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent
+ /// and signals ASP.NET to short-circuit the page execution pipeline
+ /// now that the response has been completed.
+ /// Not safe to call from ASP.NET web forms.
+ /// </summary>
+ /// <remarks>
+ /// Requires a current HttpContext.
+ /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
+ /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
+ /// Use the <see cref="Send()"/> method instead for web forms.
+ /// </remarks>
+ public virtual void Respond() {
+ Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+
+ this.Respond(HttpContext.Current);
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent
+ /// and signals ASP.NET to short-circuit the page execution pipeline
+ /// now that the response has been completed.
+ /// Not safe to call from ASP.NET web forms.
+ /// </summary>
+ /// <param name="context">The context of the HTTP request whose response should be set.
+ /// Typically this is <see cref="HttpContext.Current"/>.</param>
+ /// <remarks>
+ /// This call is not safe to make from an ASP.NET web form (.aspx file or code-behind) because
+ /// ASP.NET will render HTML after the protocol message has been sent, which will corrupt the response.
+ /// Use the <see cref="Send()"/> method instead for web forms.
+ /// </remarks>
+ public virtual void Respond(HttpContext context) {
+ Requires.NotNull(context, "context");
+
+ this.Respond(context, false);
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent.
+ /// </summary>
+ /// <param name="response">The response to set to this message.</param>
+ public virtual void Send(HttpListenerResponse response) {
+ Requires.NotNull(response, "response");
+
+ response.StatusCode = (int)this.Status;
+ MessagingUtilities.ApplyHeadersToResponse(this.Headers, response);
+ if (this.ResponseStream != null) {
+ response.ContentLength64 = this.ResponseStream.Length;
+ this.ResponseStream.CopyTo(response.OutputStream);
+ }
+
+ response.OutputStream.Close();
+ }
+
+ /// <summary>
+ /// Gets the URI that, when requested with an HTTP GET request,
+ /// would transmit the message that normally would be transmitted via a user agent redirect.
+ /// </summary>
+ /// <param name="channel">The channel to use for encoding.</param>
+ /// <returns>
+ /// The URL that would transmit the original message. This URL may exceed the normal 2K limit,
+ /// and should therefore be broken up manually and POSTed as form fields when it exceeds this length.
+ /// </returns>
+ /// <remarks>
+ /// This is useful for desktop applications that will spawn a user agent to transmit the message
+ /// rather than cause a redirect.
+ /// </remarks>
+ internal Uri GetDirectUriRequest(Channel channel) {
+ Requires.NotNull(channel, "channel");
+
+ var message = this.OriginalMessage as IDirectedProtocolMessage;
+ if (message == null) {
+ throw new InvalidOperationException(); // this only makes sense for directed messages (indirect responses)
+ }
+
+ var fields = channel.MessageDescriptions.GetAccessor(message).Serialize();
+ UriBuilder builder = new UriBuilder(message.Recipient);
+ MessagingUtilities.AppendQueryArgs(builder, fields);
+ return builder.Uri;
+ }
+
+ /// <summary>
+ /// Sets the response to some string, encoded as UTF-8.
+ /// </summary>
+ /// <param name="body">The string to set the response to.</param>
+ /// <param name="contentType">Type of the content. May be null.</param>
+ internal void SetResponse(string body, ContentType contentType) {
+ if (body == null) {
+ this.ResponseStream = null;
+ return;
+ }
+
+ if (contentType == null) {
+ contentType = new ContentType("text/html");
+ contentType.CharSet = bodyStringEncoder.WebName;
+ } else if (contentType.CharSet != bodyStringEncoder.WebName) {
+ // clone the original so we're not tampering with our inputs if it came as a parameter.
+ contentType = new ContentType(contentType.ToString());
+ contentType.CharSet = bodyStringEncoder.WebName;
+ }
+
+ this.Headers[HttpResponseHeader.ContentType] = contentType.ToString();
+ this.ResponseStream = new MemoryStream();
+ StreamWriter writer = new StreamWriter(this.ResponseStream, bodyStringEncoder);
+ writer.Write(body);
+ writer.Flush();
+ this.ResponseStream.Seek(0, SeekOrigin.Begin);
+ }
+
+ /// <summary>
+ /// Automatically sends the appropriate response to the user agent
+ /// and signals ASP.NET to short-circuit the page execution pipeline
+ /// now that the response has been completed.
+ /// </summary>
+ /// <param name="context">The context of the HTTP request whose response should be set.
+ /// Typically this is <see cref="HttpContext.Current"/>.</param>
+ /// <param name="endRequest">If set to <c>false</c>, this method calls
+ /// <see cref="HttpApplication.CompleteRequest"/> rather than <see cref="HttpResponse.End"/>
+ /// to avoid a <see cref="ThreadAbortException"/>.</param>
+ protected internal virtual void Respond(HttpContext context, bool endRequest) {
+ Requires.NotNull(context, "context");
+
+ context.Response.Clear();
+ context.Response.StatusCode = (int)this.Status;
+ MessagingUtilities.ApplyHeadersToResponse(this.Headers, context.Response);
+ if (this.ResponseStream != null) {
+ try {
+ this.ResponseStream.CopyTo(context.Response.OutputStream);
+ } catch (HttpException ex) {
+ if (ex.ErrorCode == -2147467259 && context.Response.Output != null) {
+ // Test scenarios can generate this, since the stream is being spoofed:
+ // System.Web.HttpException: OutputStream is not available when a custom TextWriter is used.
+ context.Response.Output.Write(this.Body);
+ } else {
+ throw;
+ }
+ }
+ }
+
+ if (endRequest) {
+ // This approach throws an exception in order that
+ // no more code is executed in the calling page.
+ // Microsoft no longer recommends this approach.
+ context.Response.End();
+ } else if (context.ApplicationInstance != null) {
+ // This approach doesn't throw an exception, but
+ // still tells ASP.NET to short-circuit most of the
+ // request handling pipeline to speed things up.
+ context.ApplicationInstance.CompleteRequest();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs
new file mode 100644
index 0000000..86dbb58
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs
@@ -0,0 +1,40 @@
+//-----------------------------------------------------------------------
+// <copyright file="OutgoingWebResponseActionResult.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Web.Mvc;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An ASP.NET MVC structure to represent the response to send
+ /// to the user agent when the controller has finished its work.
+ /// </summary>
+ internal class OutgoingWebResponseActionResult : ActionResult {
+ /// <summary>
+ /// The outgoing web response to send when the ActionResult is executed.
+ /// </summary>
+ private readonly OutgoingWebResponse response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutgoingWebResponseActionResult"/> class.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ internal OutgoingWebResponseActionResult(OutgoingWebResponse response) {
+ Requires.NotNull(response, "response");
+ this.response = response;
+ }
+
+ /// <summary>
+ /// Enables processing of the result of an action method by a custom type that inherits from <see cref="T:System.Web.Mvc.ActionResult"/>.
+ /// </summary>
+ /// <param name="context">The context in which to set the response.</param>
+ public override void ExecuteResult(ControllerContext context) {
+ this.response.Respond();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs b/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs
new file mode 100644
index 0000000..cf3ccb8
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolException.cs
@@ -0,0 +1,93 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProtocolException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Security;
+ using System.Security.Permissions;
+
+ /// <summary>
+ /// An exception to represent errors in the local or remote implementation of the protocol.
+ /// </summary>
+ [Serializable]
+ public class ProtocolException : Exception {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProtocolException"/> class.
+ /// </summary>
+ public ProtocolException() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProtocolException"/> class.
+ /// </summary>
+ /// <param name="message">A message describing the specific error the occurred or was detected.</param>
+ public ProtocolException(string message) : base(message) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProtocolException"/> class.
+ /// </summary>
+ /// <param name="message">A message describing the specific error the occurred or was detected.</param>
+ /// <param name="inner">The inner exception to include.</param>
+ public ProtocolException(string message, Exception inner) : base(message, inner) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProtocolException"/> class
+ /// such that it can be sent as a protocol message response to a remote caller.
+ /// </summary>
+ /// <param name="message">The human-readable exception message.</param>
+ /// <param name="faultedMessage">The message that was the cause of the exception. Must not be null.</param>
+ protected internal ProtocolException(string message, IProtocolMessage faultedMessage)
+ : base(message) {
+ Requires.NotNull(faultedMessage, "faultedMessage");
+ this.FaultedMessage = faultedMessage;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProtocolException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected ProtocolException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets the message that caused the exception.
+ /// </summary>
+ internal IProtocolMessage FaultedMessage { get; private set; }
+
+ /// <summary>
+ /// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with information about the exception.
+ /// </summary>
+ /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The <paramref name="info"/> parameter is a null reference (Nothing in Visual Basic).
+ /// </exception>
+ /// <PermissionSet>
+ /// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/>
+ /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/>
+ /// </PermissionSet>
+#if CLR4
+ [SecurityCritical]
+#else
+ [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
+#endif
+ public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) {
+ base.GetObjectData(info, context);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartEncoder.cs
new file mode 100644
index 0000000..bbb3737
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartEncoder.cs
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessagePartEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// An interface describing how various objects can be serialized and deserialized between their object and string forms.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface must include a default constructor and must be thread-safe.
+ /// </remarks>
+ [ContractClass(typeof(IMessagePartEncoderContract))]
+ public interface IMessagePartEncoder {
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>The <paramref name="value"/> in string form, ready for message transport.</returns>
+ string Encode(object value);
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>The deserialized form of the given string.</returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ object Decode(string value);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IMessagePartEncoder"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IMessagePartEncoder))]
+ internal abstract class IMessagePartEncoderContract : IMessagePartEncoder {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IMessagePartEncoderContract"/> class.
+ /// </summary>
+ protected IMessagePartEncoderContract() {
+ }
+
+ #region IMessagePartEncoder Members
+
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>
+ /// The <paramref name="value"/> in string form, ready for message transport.
+ /// </returns>
+ string IMessagePartEncoder.Encode(object value) {
+ Requires.NotNull(value, "value");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>
+ /// The deserialized form of the given string.
+ /// </returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ object IMessagePartEncoder.Decode(string value) {
+ Requires.NotNull(value, "value");
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartNullEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartNullEncoder.cs
new file mode 100644
index 0000000..7581550
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartNullEncoder.cs
@@ -0,0 +1,18 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessagePartNullEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ /// <summary>
+ /// A message part encoder that has a special encoding for a null value.
+ /// </summary>
+ public interface IMessagePartNullEncoder : IMessagePartEncoder {
+ /// <summary>
+ /// Gets the string representation to include in a serialized message
+ /// when the message part has a <c>null</c> value.
+ /// </summary>
+ string EncodedNullValue { get; }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartOriginalEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartOriginalEncoder.cs
new file mode 100644
index 0000000..9ad55c9
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartOriginalEncoder.cs
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------
+// <copyright file="IMessagePartOriginalEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ /// <summary>
+ /// An interface describing how various objects can be serialized and deserialized between their object and string forms.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface must include a default constructor and must be thread-safe.
+ /// </remarks>
+ public interface IMessagePartOriginalEncoder : IMessagePartEncoder {
+ /// <summary>
+ /// Encodes the specified value as the original value that was formerly decoded.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>The <paramref name="value"/> in string form, ready for message transport.</returns>
+ string EncodeAsOriginalString(object value);
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescription.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescription.cs
new file mode 100644
index 0000000..9a8098b
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescription.cs
@@ -0,0 +1,283 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDescription.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Reflection;
+
+ /// <summary>
+ /// A mapping between serialized key names and <see cref="MessagePart"/> instances describing
+ /// those key/values pairs.
+ /// </summary>
+ internal class MessageDescription {
+ /// <summary>
+ /// A mapping between the serialized key names and their
+ /// describing <see cref="MessagePart"/> instances.
+ /// </summary>
+ private Dictionary<string, MessagePart> mapping;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageDescription"/> class.
+ /// </summary>
+ /// <param name="messageType">Type of the message.</param>
+ /// <param name="messageVersion">The message version.</param>
+ internal MessageDescription(Type messageType, Version messageVersion) {
+ Requires.NotNullSubtype<IMessage>(messageType, "messageType");
+ Requires.NotNull(messageVersion, "messageVersion");
+
+ this.MessageType = messageType;
+ this.MessageVersion = messageVersion;
+ this.ReflectMessageType();
+ }
+
+ /// <summary>
+ /// Gets the mapping between the serialized key names and their describing
+ /// <see cref="MessagePart"/> instances.
+ /// </summary>
+ internal IDictionary<string, MessagePart> Mapping {
+ get { return this.mapping; }
+ }
+
+ /// <summary>
+ /// Gets the message version this instance was generated from.
+ /// </summary>
+ internal Version MessageVersion { get; private set; }
+
+ /// <summary>
+ /// Gets the type of message this instance was generated from.
+ /// </summary>
+ /// <value>The type of the described message.</value>
+ internal Type MessageType { get; private set; }
+
+ /// <summary>
+ /// Gets the constructors available on the message type.
+ /// </summary>
+ internal ConstructorInfo[] Constructors { get; private set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String"/> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String"/> that represents this instance.
+ /// </returns>
+ public override string ToString() {
+ return this.MessageType.Name + " (" + this.MessageVersion + ")";
+ }
+
+ /// <summary>
+ /// Gets a dictionary that provides read/write access to a message.
+ /// </summary>
+ /// <param name="message">The message the dictionary should provide access to.</param>
+ /// <returns>The dictionary accessor to the message</returns>
+ [Pure]
+ internal MessageDictionary GetDictionary(IMessage message) {
+ Requires.NotNull(message, "message");
+ Contract.Ensures(Contract.Result<MessageDictionary>() != null);
+ return this.GetDictionary(message, false);
+ }
+
+ /// <summary>
+ /// Gets a dictionary that provides read/write access to a message.
+ /// </summary>
+ /// <param name="message">The message the dictionary should provide access to.</param>
+ /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param>
+ /// <returns>The dictionary accessor to the message</returns>
+ [Pure]
+ internal MessageDictionary GetDictionary(IMessage message, bool getOriginalValues) {
+ Requires.NotNull(message, "message");
+ Contract.Ensures(Contract.Result<MessageDictionary>() != null);
+ return new MessageDictionary(message, this, getOriginalValues);
+ }
+
+ /// <summary>
+ /// Ensures the message parts pass basic validation.
+ /// </summary>
+ /// <param name="parts">The key/value pairs of the serialized message.</param>
+ internal void EnsureMessagePartsPassBasicValidation(IDictionary<string, string> parts) {
+ try {
+ this.CheckRequiredMessagePartsArePresent(parts.Keys, true);
+ this.CheckRequiredProtocolMessagePartsAreNotEmpty(parts, true);
+ this.CheckMessagePartsConstantValues(parts, true);
+ } catch (ProtocolException) {
+ Logger.Messaging.ErrorFormat(
+ "Error while performing basic validation of {0} with these message parts:{1}{2}",
+ this.MessageType.Name,
+ Environment.NewLine,
+ parts.ToStringDeferred());
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Tests whether all the required message parts pass basic validation for the given data.
+ /// </summary>
+ /// <param name="parts">The key/value pairs of the serialized message.</param>
+ /// <returns>A value indicating whether the provided data fits the message's basic requirements.</returns>
+ internal bool CheckMessagePartsPassBasicValidation(IDictionary<string, string> parts) {
+ Requires.NotNull(parts, "parts");
+
+ return this.CheckRequiredMessagePartsArePresent(parts.Keys, false) &&
+ this.CheckRequiredProtocolMessagePartsAreNotEmpty(parts, false) &&
+ this.CheckMessagePartsConstantValues(parts, false);
+ }
+
+ /// <summary>
+ /// Verifies that a given set of keys include all the required parameters
+ /// for this message type or throws an exception.
+ /// </summary>
+ /// <param name="keys">The names of all parameters included in a message.</param>
+ /// <param name="throwOnFailure">if set to <c>true</c> an exception is thrown on failure with details.</param>
+ /// <returns>A value indicating whether the provided data fits the message's basic requirements.</returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when required parts of a message are not in <paramref name="keys"/>
+ /// if <paramref name="throwOnFailure"/> is <c>true</c>.
+ /// </exception>
+ private bool CheckRequiredMessagePartsArePresent(IEnumerable<string> keys, bool throwOnFailure) {
+ Requires.NotNull(keys, "keys");
+
+ var missingKeys = (from part in this.Mapping.Values
+ where part.IsRequired && !keys.Contains(part.Name)
+ select part.Name).ToArray();
+ if (missingKeys.Length > 0) {
+ if (throwOnFailure) {
+ ErrorUtilities.ThrowProtocol(
+ MessagingStrings.RequiredParametersMissing,
+ this.MessageType.FullName,
+ string.Join(", ", missingKeys));
+ } else {
+ Logger.Messaging.DebugFormat(
+ MessagingStrings.RequiredParametersMissing,
+ this.MessageType.FullName,
+ missingKeys.ToStringDeferred());
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Ensures the protocol message parts that must not be empty are in fact not empty.
+ /// </summary>
+ /// <param name="partValues">A dictionary of key/value pairs that make up the serialized message.</param>
+ /// <param name="throwOnFailure">if set to <c>true</c> an exception is thrown on failure with details.</param>
+ /// <returns>A value indicating whether the provided data fits the message's basic requirements.</returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when required parts of a message are not in <paramref name="partValues"/>
+ /// if <paramref name="throwOnFailure"/> is <c>true</c>.
+ /// </exception>
+ private bool CheckRequiredProtocolMessagePartsAreNotEmpty(IDictionary<string, string> partValues, bool throwOnFailure) {
+ Requires.NotNull(partValues, "partValues");
+
+ string value;
+ var emptyValuedKeys = (from part in this.Mapping.Values
+ where !part.AllowEmpty && partValues.TryGetValue(part.Name, out value) && value != null && value.Length == 0
+ select part.Name).ToArray();
+ if (emptyValuedKeys.Length > 0) {
+ if (throwOnFailure) {
+ ErrorUtilities.ThrowProtocol(
+ MessagingStrings.RequiredNonEmptyParameterWasEmpty,
+ this.MessageType.FullName,
+ string.Join(", ", emptyValuedKeys));
+ } else {
+ Logger.Messaging.DebugFormat(
+ MessagingStrings.RequiredNonEmptyParameterWasEmpty,
+ this.MessageType.FullName,
+ emptyValuedKeys.ToStringDeferred());
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Checks that a bunch of message part values meet the constant value requirements of this message description.
+ /// </summary>
+ /// <param name="partValues">The part values.</param>
+ /// <param name="throwOnFailure">if set to <c>true</c>, this method will throw on failure.</param>
+ /// <returns>A value indicating whether all the requirements are met.</returns>
+ private bool CheckMessagePartsConstantValues(IDictionary<string, string> partValues, bool throwOnFailure) {
+ Requires.NotNull(partValues, "partValues");
+
+ var badConstantValues = (from part in this.Mapping.Values
+ where part.IsConstantValueAvailableStatically
+ where partValues.ContainsKey(part.Name)
+ where !string.Equals(partValues[part.Name], part.StaticConstantValue, StringComparison.Ordinal)
+ select part.Name).ToArray();
+ if (badConstantValues.Length > 0) {
+ if (throwOnFailure) {
+ ErrorUtilities.ThrowProtocol(
+ MessagingStrings.RequiredMessagePartConstantIncorrect,
+ this.MessageType.FullName,
+ string.Join(", ", badConstantValues));
+ } else {
+ Logger.Messaging.DebugFormat(
+ MessagingStrings.RequiredMessagePartConstantIncorrect,
+ this.MessageType.FullName,
+ badConstantValues.ToStringDeferred());
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Reflects over some <see cref="IMessage"/>-implementing type
+ /// and prepares to serialize/deserialize instances of that type.
+ /// </summary>
+ private void ReflectMessageType() {
+ this.mapping = new Dictionary<string, MessagePart>();
+
+ Type currentType = this.MessageType;
+ do {
+ foreach (MemberInfo member in currentType.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) {
+ if (member is PropertyInfo || member is FieldInfo) {
+ MessagePartAttribute partAttribute =
+ (from a in member.GetCustomAttributes(typeof(MessagePartAttribute), true).OfType<MessagePartAttribute>()
+ orderby a.MinVersionValue descending
+ where a.MinVersionValue <= this.MessageVersion
+ where a.MaxVersionValue >= this.MessageVersion
+ select a).FirstOrDefault();
+ if (partAttribute != null) {
+ MessagePart part = new MessagePart(member, partAttribute);
+ if (this.mapping.ContainsKey(part.Name)) {
+ Logger.Messaging.WarnFormat(
+ "Message type {0} has more than one message part named {1}. Inherited members will be hidden.",
+ this.MessageType.Name,
+ part.Name);
+ } else {
+ this.mapping.Add(part.Name, part);
+ }
+ }
+ }
+ }
+ currentType = currentType.BaseType;
+ } while (currentType != null);
+
+ BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
+ this.Constructors = this.MessageType.GetConstructors(flags);
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Describes traits of this class that are always true.
+ /// </summary>
+ [ContractInvariantMethod]
+ private void Invariant() {
+ Contract.Invariant(this.MessageType != null);
+ Contract.Invariant(this.MessageVersion != null);
+ Contract.Invariant(this.Constructors != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescriptionCollection.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescriptionCollection.cs
new file mode 100644
index 0000000..79ef172
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDescriptionCollection.cs
@@ -0,0 +1,217 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDescriptionCollection.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A cache of <see cref="MessageDescription"/> instances.
+ /// </summary>
+ [ContractVerification(true)]
+ internal class MessageDescriptionCollection : IEnumerable<MessageDescription> {
+ /// <summary>
+ /// A dictionary of reflected message types and the generated reflection information.
+ /// </summary>
+ private readonly Dictionary<MessageTypeAndVersion, MessageDescription> reflectedMessageTypes = new Dictionary<MessageTypeAndVersion, MessageDescription>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageDescriptionCollection"/> class.
+ /// </summary>
+ internal MessageDescriptionCollection() {
+ }
+
+ #region IEnumerable<MessageDescription> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<MessageDescription> GetEnumerator() {
+ return this.reflectedMessageTypes.Values.GetEnumerator();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return this.reflectedMessageTypes.Values.GetEnumerator();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets a <see cref="MessageDescription"/> instance prepared for the
+ /// given message type.
+ /// </summary>
+ /// <param name="messageType">A type that implements <see cref="IMessage"/>.</param>
+ /// <param name="messageVersion">The protocol version of the message.</param>
+ /// <returns>A <see cref="MessageDescription"/> instance.</returns>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Assume(System.Boolean,System.String,System.String)", Justification = "No localization required.")]
+ [Pure]
+ internal MessageDescription Get(Type messageType, Version messageVersion) {
+ Requires.NotNullSubtype<IMessage>(messageType, "messageType");
+ Requires.NotNull(messageVersion, "messageVersion");
+ Contract.Ensures(Contract.Result<MessageDescription>() != null);
+
+ MessageTypeAndVersion key = new MessageTypeAndVersion(messageType, messageVersion);
+
+ MessageDescription result;
+ if (!this.reflectedMessageTypes.TryGetValue(key, out result)) {
+ lock (this.reflectedMessageTypes) {
+ if (!this.reflectedMessageTypes.TryGetValue(key, out result)) {
+ this.reflectedMessageTypes[key] = result = new MessageDescription(messageType, messageVersion);
+ }
+ }
+ }
+
+ Contract.Assume(result != null, "We should never assign null values to this dictionary.");
+ return result;
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MessageDescription"/> instance prepared for the
+ /// given message type.
+ /// </summary>
+ /// <param name="message">The message for which a <see cref="MessageDescription"/> should be obtained.</param>
+ /// <returns>
+ /// A <see cref="MessageDescription"/> instance.
+ /// </returns>
+ [Pure]
+ internal MessageDescription Get(IMessage message) {
+ Requires.NotNull(message, "message");
+ Contract.Ensures(Contract.Result<MessageDescription>() != null);
+ return this.Get(message.GetType(), message.Version);
+ }
+
+ /// <summary>
+ /// Gets the dictionary that provides read/write access to a message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>The dictionary.</returns>
+ [Pure]
+ internal MessageDictionary GetAccessor(IMessage message) {
+ Requires.NotNull(message, "message");
+ return this.GetAccessor(message, false);
+ }
+
+ /// <summary>
+ /// Gets the dictionary that provides read/write access to a message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param>
+ /// <returns>The dictionary.</returns>
+ [Pure]
+ internal MessageDictionary GetAccessor(IMessage message, bool getOriginalValues) {
+ Requires.NotNull(message, "message");
+ return this.Get(message).GetDictionary(message, getOriginalValues);
+ }
+
+ /// <summary>
+ /// A struct used as the key to bundle message type and version.
+ /// </summary>
+ [ContractVerification(true)]
+ private struct MessageTypeAndVersion {
+ /// <summary>
+ /// Backing store for the <see cref="Type"/> property.
+ /// </summary>
+ private readonly Type type;
+
+ /// <summary>
+ /// Backing store for the <see cref="Version"/> property.
+ /// </summary>
+ private readonly Version version;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageTypeAndVersion"/> struct.
+ /// </summary>
+ /// <param name="messageType">Type of the message.</param>
+ /// <param name="messageVersion">The message version.</param>
+ internal MessageTypeAndVersion(Type messageType, Version messageVersion) {
+ Requires.NotNull(messageType, "messageType");
+ Requires.NotNull(messageVersion, "messageVersion");
+
+ this.type = messageType;
+ this.version = messageVersion;
+ }
+
+ /// <summary>
+ /// Gets the message type.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Exposes basic identity on the type.")]
+ internal Type Type {
+ get { return this.type; }
+ }
+
+ /// <summary>
+ /// Gets the message version.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Exposes basic identity on the type.")]
+ internal Version Version {
+ get { return this.version; }
+ }
+
+ /// <summary>
+ /// Implements the operator ==.
+ /// </summary>
+ /// <param name="first">The first object to compare.</param>
+ /// <param name="second">The second object to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator ==(MessageTypeAndVersion first, MessageTypeAndVersion second) {
+ // structs cannot be null, so this is safe
+ return first.Equals(second);
+ }
+
+ /// <summary>
+ /// Implements the operator !=.
+ /// </summary>
+ /// <param name="first">The first object to compare.</param>
+ /// <param name="second">The second object to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator !=(MessageTypeAndVersion first, MessageTypeAndVersion second) {
+ // structs cannot be null, so this is safe
+ return !first.Equals(second);
+ }
+
+ /// <summary>
+ /// Indicates whether this instance and a specified object are equal.
+ /// </summary>
+ /// <param name="obj">Another object to compare to.</param>
+ /// <returns>
+ /// true if <paramref name="obj"/> and this instance are the same type and represent the same value; otherwise, false.
+ /// </returns>
+ public override bool Equals(object obj) {
+ if (obj is MessageTypeAndVersion) {
+ MessageTypeAndVersion other = (MessageTypeAndVersion)obj;
+ return this.type == other.type && this.version == other.version;
+ } else {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Returns the hash code for this instance.
+ /// </summary>
+ /// <returns>
+ /// A 32-bit signed integer that is the hash code for this instance.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.type.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDictionary.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDictionary.cs
new file mode 100644
index 0000000..54e2dd5
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessageDictionary.cs
@@ -0,0 +1,409 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessageDictionary.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Wraps an <see cref="IMessage"/> instance in a dictionary that
+ /// provides access to both well-defined message properties and "extra"
+ /// name/value pairs that have no properties associated with them.
+ /// </summary>
+ [ContractVerification(false)]
+ internal class MessageDictionary : IDictionary<string, string> {
+ /// <summary>
+ /// The <see cref="IMessage"/> instance manipulated by this dictionary.
+ /// </summary>
+ private readonly IMessage message;
+
+ /// <summary>
+ /// The <see cref="MessageDescription"/> instance that describes the message type.
+ /// </summary>
+ private readonly MessageDescription description;
+
+ /// <summary>
+ /// Whether original string values should be retrieved instead of normalized ones.
+ /// </summary>
+ private readonly bool getOriginalValues;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageDictionary"/> class.
+ /// </summary>
+ /// <param name="message">The message instance whose values will be manipulated by this dictionary.</param>
+ /// <param name="description">The message description.</param>
+ /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param>
+ [Pure]
+ internal MessageDictionary(IMessage message, MessageDescription description, bool getOriginalValues) {
+ Requires.NotNull(message, "message");
+ Requires.NotNull(description, "description");
+
+ this.message = message;
+ this.description = description;
+ this.getOriginalValues = getOriginalValues;
+ }
+
+ /// <summary>
+ /// Gets the message this dictionary provides access to.
+ /// </summary>
+ public IMessage Message {
+ get {
+ Contract.Ensures(Contract.Result<IMessage>() != null);
+ return this.message;
+ }
+ }
+
+ /// <summary>
+ /// Gets the description of the type of message this dictionary provides access to.
+ /// </summary>
+ public MessageDescription Description {
+ get {
+ Contract.Ensures(Contract.Result<MessageDescription>() != null);
+ return this.description;
+ }
+ }
+
+ #region ICollection<KeyValuePair<string,string>> Properties
+
+ /// <summary>
+ /// Gets the number of explicitly set values in the message.
+ /// </summary>
+ public int Count {
+ get { return this.Keys.Count; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this message is read only.
+ /// </summary>
+ bool ICollection<KeyValuePair<string, string>>.IsReadOnly {
+ get { return false; }
+ }
+
+ #endregion
+
+ #region IDictionary<string,string> Properties
+
+ /// <summary>
+ /// Gets all the keys that have values associated with them.
+ /// </summary>
+ public ICollection<string> Keys {
+ get {
+ List<string> keys = new List<string>(this.message.ExtraData.Count + this.description.Mapping.Count);
+ keys.AddRange(this.DeclaredKeys);
+ keys.AddRange(this.AdditionalKeys);
+ return keys.AsReadOnly();
+ }
+ }
+
+ /// <summary>
+ /// Gets the set of official message part names that have non-null values associated with them.
+ /// </summary>
+ public ICollection<string> DeclaredKeys {
+ get {
+ List<string> keys = new List<string>(this.description.Mapping.Count);
+ foreach (var pair in this.description.Mapping) {
+ // Don't include keys with null values, but default values for structs is ok
+ if (pair.Value.GetValue(this.message, this.getOriginalValues) != null) {
+ keys.Add(pair.Key);
+ }
+ }
+
+ return keys.AsReadOnly();
+ }
+ }
+
+ /// <summary>
+ /// Gets the keys that are in the message but not declared as official OAuth properties.
+ /// </summary>
+ public ICollection<string> AdditionalKeys {
+ get { return this.message.ExtraData.Keys; }
+ }
+
+ /// <summary>
+ /// Gets all the values.
+ /// </summary>
+ public ICollection<string> Values {
+ get {
+ List<string> values = new List<string>(this.message.ExtraData.Count + this.description.Mapping.Count);
+ foreach (MessagePart part in this.description.Mapping.Values) {
+ if (part.GetValue(this.message, this.getOriginalValues) != null) {
+ values.Add(part.GetValue(this.message, this.getOriginalValues));
+ }
+ }
+
+ foreach (string value in this.message.ExtraData.Values) {
+ Debug.Assert(value != null, "Null values should never be allowed in the extra data dictionary.");
+ values.Add(value);
+ }
+
+ return values.AsReadOnly();
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the serializer for the message this dictionary provides access to.
+ /// </summary>
+ private MessageSerializer Serializer {
+ get { return MessageSerializer.Get(this.Message.GetType()); }
+ }
+
+ #region IDictionary<string,string> Indexers
+
+ /// <summary>
+ /// Gets or sets a value for some named value.
+ /// </summary>
+ /// <param name="key">The serialized form of a name for the value to read or write.</param>
+ /// <returns>The named value.</returns>
+ /// <remarks>
+ /// If the key matches a declared property or field on the message type,
+ /// that type member is set. Otherwise the key/value is stored in a
+ /// dictionary for extra (weakly typed) strings.
+ /// </remarks>
+ /// <exception cref="ArgumentException">Thrown when setting a value that is not allowed for a given <paramref name="key"/>.</exception>
+ public string this[string key] {
+ get {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ // Never throw KeyNotFoundException for declared properties.
+ return part.GetValue(this.message, this.getOriginalValues);
+ } else {
+ return this.message.ExtraData[key];
+ }
+ }
+
+ set {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ part.SetValue(this.message, value);
+ } else {
+ if (value == null) {
+ this.message.ExtraData.Remove(key);
+ } else {
+ this.message.ExtraData[key] = value;
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region IDictionary<string,string> Methods
+
+ /// <summary>
+ /// Adds a named value to the message.
+ /// </summary>
+ /// <param name="key">The serialized form of the name whose value is being set.</param>
+ /// <param name="value">The serialized form of the value.</param>
+ /// <exception cref="ArgumentException">
+ /// Thrown if <paramref name="key"/> already has a set value in this message.
+ /// </exception>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown if <paramref name="value"/> is null.
+ /// </exception>
+ public void Add(string key, string value) {
+ ErrorUtilities.VerifyArgumentNotNull(value, "value");
+
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ if (part.IsNondefaultValueSet(this.message)) {
+ throw new ArgumentException(MessagingStrings.KeyAlreadyExists);
+ }
+ part.SetValue(this.message, value);
+ } else {
+ this.message.ExtraData.Add(key, value);
+ }
+ }
+
+ /// <summary>
+ /// Checks whether some named parameter has a value set in the message.
+ /// </summary>
+ /// <param name="key">The serialized form of the message part's name.</param>
+ /// <returns>True if the parameter by the given name has a set value. False otherwise.</returns>
+ public bool ContainsKey(string key) {
+ return this.message.ExtraData.ContainsKey(key) ||
+ (this.description.Mapping.ContainsKey(key) && this.description.Mapping[key].GetValue(this.message, this.getOriginalValues) != null);
+ }
+
+ /// <summary>
+ /// Removes a name and value from the message given its name.
+ /// </summary>
+ /// <param name="key">The serialized form of the name to remove.</param>
+ /// <returns>True if a message part by the given name was found and removed. False otherwise.</returns>
+ public bool Remove(string key) {
+ if (this.message.ExtraData.Remove(key)) {
+ return true;
+ } else {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ if (part.GetValue(this.message, this.getOriginalValues) != null) {
+ part.SetValue(this.message, null);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Gets some named value if the key has a value.
+ /// </summary>
+ /// <param name="key">The name (in serialized form) of the value being sought.</param>
+ /// <param name="value">The variable where the value will be set.</param>
+ /// <returns>True if the key was found and <paramref name="value"/> was set. False otherwise.</returns>
+ public bool TryGetValue(string key, out string value) {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(key, out part)) {
+ value = part.GetValue(this.message, this.getOriginalValues);
+ return value != null;
+ }
+ return this.message.ExtraData.TryGetValue(key, out value);
+ }
+
+ #endregion
+
+ #region ICollection<KeyValuePair<string,string>> Methods
+
+ /// <summary>
+ /// Sets a named value in the message.
+ /// </summary>
+ /// <param name="item">The name-value pair to add. The name is the serialized form of the key.</param>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")]
+ public void Add(KeyValuePair<string, string> item) {
+ this.Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Removes all values in the message.
+ /// </summary>
+ public void ClearValues() {
+ foreach (string key in this.Keys) {
+ this.Remove(key);
+ }
+ }
+
+ /// <summary>
+ /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
+ /// </summary>
+ /// <exception cref="T:System.NotSupportedException">
+ /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
+ /// </exception>
+ /// <remarks>
+ /// This method cannot be implemented because keys are not guaranteed to be removed
+ /// since some are inherent to the type of message that this dictionary provides
+ /// access to.
+ /// </remarks>
+ public void Clear() {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Checks whether a named value has been set on the message.
+ /// </summary>
+ /// <param name="item">The name/value pair.</param>
+ /// <returns>True if the key exists and has the given value. False otherwise.</returns>
+ public bool Contains(KeyValuePair<string, string> item) {
+ MessagePart part;
+ if (this.description.Mapping.TryGetValue(item.Key, out part)) {
+ return string.Equals(part.GetValue(this.message, this.getOriginalValues), item.Value, StringComparison.Ordinal);
+ } else {
+ return this.message.ExtraData.Contains(item);
+ }
+ }
+
+ /// <summary>
+ /// Copies all the serializable data from the message to a key/value array.
+ /// </summary>
+ /// <param name="array">The array to copy to.</param>
+ /// <param name="arrayIndex">The index in the <paramref name="array"/> to begin copying to.</param>
+ void ICollection<KeyValuePair<string, string>>.CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) {
+ foreach (var pair in (IDictionary<string, string>)this) {
+ array[arrayIndex++] = pair;
+ }
+ }
+
+ /// <summary>
+ /// Removes a named value from the message if it exists.
+ /// </summary>
+ /// <param name="item">The serialized form of the name and value to remove.</param>
+ /// <returns>True if the name/value was found and removed. False otherwise.</returns>
+ public bool Remove(KeyValuePair<string, string> item) {
+ // We use contains because that checks that the value is equal as well.
+ if (((ICollection<KeyValuePair<string, string>>)this).Contains(item)) {
+ ((IDictionary<string, string>)this).Remove(item.Key);
+ return true;
+ }
+ return false;
+ }
+
+ #endregion
+
+ #region IEnumerable<KeyValuePair<string,string>> Members
+
+ /// <summary>
+ /// Gets an enumerator that generates KeyValuePair&lt;string, string&gt; instances
+ /// for all the key/value pairs that are set in the message.
+ /// </summary>
+ /// <returns>The enumerator that can generate the name/value pairs.</returns>
+ public IEnumerator<KeyValuePair<string, string>> GetEnumerator() {
+ foreach (string key in this.Keys) {
+ yield return new KeyValuePair<string, string>(key, this[key]);
+ }
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Gets an enumerator that generates KeyValuePair&lt;string, string&gt; instances
+ /// for all the key/value pairs that are set in the message.
+ /// </summary>
+ /// <returns>The enumerator that can generate the name/value pairs.</returns>
+ IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return ((IEnumerable<KeyValuePair<string, string>>)this).GetEnumerator();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Saves the data in a message to a standard dictionary.
+ /// </summary>
+ /// <returns>The generated dictionary.</returns>
+ [Pure]
+ public IDictionary<string, string> Serialize() {
+ Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null);
+ return this.Serializer.Serialize(this);
+ }
+
+ /// <summary>
+ /// Loads data from a dictionary into the message.
+ /// </summary>
+ /// <param name="fields">The data to load into the message.</param>
+ public void Deserialize(IDictionary<string, string> fields) {
+ Requires.NotNull(fields, "fields");
+ this.Serializer.Deserialize(fields, this);
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [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.Message != null);
+ Contract.Invariant(this.Description != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs
new file mode 100644
index 0000000..f439c4d
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs
@@ -0,0 +1,428 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagePart.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net.Security;
+ using System.Reflection;
+ using System.Xml;
+ using DotNetOpenAuth.Configuration;
+
+ /// <summary>
+ /// Describes an individual member of a message and assists in its serialization.
+ /// </summary>
+ [ContractVerification(true)]
+ [DebuggerDisplay("MessagePart {Name}")]
+ internal class MessagePart {
+ /// <summary>
+ /// A map of converters that help serialize custom objects to string values and back again.
+ /// </summary>
+ private static readonly Dictionary<Type, ValueMapping> converters = new Dictionary<Type, ValueMapping>();
+
+ /// <summary>
+ /// A map of instantiated custom encoders used to encode/decode message parts.
+ /// </summary>
+ private static readonly Dictionary<Type, IMessagePartEncoder> encoders = new Dictionary<Type, IMessagePartEncoder>();
+
+ /// <summary>
+ /// The string-object conversion routines to use for this individual message part.
+ /// </summary>
+ private ValueMapping converter;
+
+ /// <summary>
+ /// The property that this message part is associated with, if aplicable.
+ /// </summary>
+ private PropertyInfo property;
+
+ /// <summary>
+ /// The field that this message part is associated with, if aplicable.
+ /// </summary>
+ private FieldInfo field;
+
+ /// <summary>
+ /// The type of the message part. (Not the type of the message itself).
+ /// </summary>
+ private Type memberDeclaredType;
+
+ /// <summary>
+ /// The default (uninitialized) value of the member inherent in its type.
+ /// </summary>
+ private object defaultMemberValue;
+
+ /// <summary>
+ /// Initializes static members of the <see cref="MessagePart"/> class.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This simplifies the rest of the code.")]
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "By design.")]
+ [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Much more efficient initialization when we can call methods.")]
+ static MessagePart() {
+ Func<string, Uri> safeUri = str => {
+ Contract.Assume(str != null);
+ return new Uri(str);
+ };
+ Func<string, bool> safeBool = str => {
+ Contract.Assume(str != null);
+ return bool.Parse(str);
+ };
+
+ Func<byte[], string> safeFromByteArray = bytes => {
+ Contract.Assume(bytes != null);
+ return Convert.ToBase64String(bytes);
+ };
+ Func<string, byte[]> safeToByteArray = str => {
+ Contract.Assume(str != null);
+ return Convert.FromBase64String(str);
+ };
+ Map<Uri>(uri => uri.AbsoluteUri, uri => uri.OriginalString, safeUri);
+ Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), null, str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc));
+ Map<TimeSpan>(ts => ts.ToString(), null, str => TimeSpan.Parse(str));
+ Map<byte[]>(safeFromByteArray, null, safeToByteArray);
+ Map<bool>(value => value.ToString().ToLowerInvariant(), null, safeBool);
+ Map<CultureInfo>(c => c.Name, null, str => new CultureInfo(str));
+ Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), null, str => str.Split(',').Select(s => new CultureInfo(s)).ToArray());
+ Map<Type>(t => t.FullName, null, str => Type.GetType(str));
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessagePart"/> class.
+ /// </summary>
+ /// <param name="member">
+ /// A property or field of an <see cref="IMessage"/> implementing type
+ /// that has a <see cref="MessagePartAttribute"/> attached to it.
+ /// </param>
+ /// <param name="attribute">
+ /// The attribute discovered on <paramref name="member"/> that describes the
+ /// serialization requirements of the message part.
+ /// </param>
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Unavoidable"), SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code contracts requires it.")]
+ internal MessagePart(MemberInfo member, MessagePartAttribute attribute) {
+ Requires.NotNull(member, "member");
+ Requires.True(member is FieldInfo || member is PropertyInfo, "member");
+ Requires.NotNull(attribute, "attribute");
+
+ this.field = member as FieldInfo;
+ this.property = member as PropertyInfo;
+ this.Name = attribute.Name ?? member.Name;
+ this.RequiredProtection = attribute.RequiredProtection;
+ this.IsRequired = attribute.IsRequired;
+ this.AllowEmpty = attribute.AllowEmpty;
+ this.memberDeclaredType = (this.field != null) ? this.field.FieldType : this.property.PropertyType;
+ this.defaultMemberValue = DeriveDefaultValue(this.memberDeclaredType);
+
+ Contract.Assume(this.memberDeclaredType != null); // CC missing PropertyInfo.PropertyType ensures result != null
+ if (attribute.Encoder == null) {
+ if (!converters.TryGetValue(this.memberDeclaredType, out this.converter)) {
+ if (this.memberDeclaredType.IsGenericType &&
+ this.memberDeclaredType.GetGenericTypeDefinition() == typeof(Nullable<>)) {
+ // It's a nullable type. Try again to look up an appropriate converter for the underlying type.
+ Type underlyingType = Nullable.GetUnderlyingType(this.memberDeclaredType);
+ ValueMapping underlyingMapping;
+ if (converters.TryGetValue(underlyingType, out underlyingMapping)) {
+ this.converter = new ValueMapping(
+ underlyingMapping.ValueToString,
+ null,
+ str => str != null ? underlyingMapping.StringToValue(str) : null);
+ } else {
+ this.converter = new ValueMapping(
+ obj => obj != null ? obj.ToString() : null,
+ null,
+ str => str != null ? Convert.ChangeType(str, underlyingType, CultureInfo.InvariantCulture) : null);
+ }
+ } else {
+ this.converter = new ValueMapping(
+ obj => obj != null ? obj.ToString() : null,
+ null,
+ str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null);
+ }
+ }
+ } else {
+ this.converter = new ValueMapping(GetEncoder(attribute.Encoder));
+ }
+
+ // readonly and const fields are considered legal, and "constants" for message transport.
+ FieldAttributes constAttributes = FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault;
+ if (this.field != null && (
+ (this.field.Attributes & FieldAttributes.InitOnly) == FieldAttributes.InitOnly ||
+ (this.field.Attributes & constAttributes) == constAttributes)) {
+ this.IsConstantValue = true;
+ this.IsConstantValueAvailableStatically = this.field.IsStatic;
+ } else if (this.property != null && !this.property.CanWrite) {
+ this.IsConstantValue = true;
+ }
+
+ // Validate a sane combination of settings
+ this.ValidateSettings();
+ }
+
+ /// <summary>
+ /// Gets or sets the name to use when serializing or deserializing this parameter in a message.
+ /// </summary>
+ internal string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether this message part must be signed.
+ /// </summary>
+ internal ProtectionLevel RequiredProtection { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this message part is required for the
+ /// containing message to be valid.
+ /// </summary>
+ internal bool IsRequired { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the string value is allowed to be empty in the serialized message.
+ /// </summary>
+ internal bool AllowEmpty { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the field or property must remain its default value.
+ /// </summary>
+ internal bool IsConstantValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this part is defined as a constant field and can be read without a message instance.
+ /// </summary>
+ internal bool IsConstantValueAvailableStatically { get; set; }
+
+ /// <summary>
+ /// Gets the static constant value for this message part without a message instance.
+ /// </summary>
+ internal string StaticConstantValue {
+ get {
+ Requires.ValidState(this.IsConstantValueAvailableStatically);
+ return this.ToString(this.field.GetValue(null), false);
+ }
+ }
+
+ /// <summary>
+ /// Gets the type of the declared member.
+ /// </summary>
+ internal Type MemberDeclaredType {
+ get { return this.memberDeclaredType; }
+ }
+
+ /// <summary>
+ /// Adds a pair of type conversion functions to the static conversion map.
+ /// </summary>
+ /// <typeparam name="T">The custom type to convert to and from strings.</typeparam>
+ /// <param name="toString">The function to convert the custom type to a string.</param>
+ /// <param name="toOriginalString">The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the <paramref name="toString"/> function.</param>
+ /// <param name="toValue">The function to convert a string to the custom type.</param>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "toString", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "toValue", Justification = "Code contracts")]
+ internal static void Map<T>(Func<T, string> toString, Func<T, string> toOriginalString, Func<string, T> toValue) {
+ Requires.NotNull(toString, "toString");
+ Requires.NotNull(toValue, "toValue");
+
+ if (toOriginalString == null) {
+ toOriginalString = toString;
+ }
+
+ Func<object, string> safeToString = obj => obj != null ? toString((T)obj) : null;
+ Func<object, string> safeToOriginalString = obj => obj != null ? toOriginalString((T)obj) : null;
+ Func<string, object> safeToT = str => str != null ? toValue(str) : default(T);
+ converters.Add(typeof(T), new ValueMapping(safeToString, safeToOriginalString, safeToT));
+ }
+
+ /// <summary>
+ /// Sets the member of a given message to some given value.
+ /// Used in deserialization.
+ /// </summary>
+ /// <param name="message">The message instance containing the member whose value should be set.</param>
+ /// <param name="value">The string representation of the value to set.</param>
+ internal void SetValue(IMessage message, string value) {
+ Requires.NotNull(message, "message");
+
+ try {
+ if (this.IsConstantValue) {
+ string constantValue = this.GetValue(message);
+ var caseSensitivity = DotNetOpenAuthSection.Messaging.Strict ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+ if (!string.Equals(constantValue, value, caseSensitivity)) {
+ throw new ArgumentException(string.Format(
+ CultureInfo.CurrentCulture,
+ MessagingStrings.UnexpectedMessagePartValueForConstant,
+ message.GetType().Name,
+ this.Name,
+ constantValue,
+ value));
+ }
+ } else {
+ this.SetValueAsObject(message, this.ToValue(value));
+ }
+ } catch (Exception ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartReadFailure, message.GetType(), this.Name, value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the normalized form of a value of a member of a given message.
+ /// Used in serialization.
+ /// </summary>
+ /// <param name="message">The message instance to read the value from.</param>
+ /// <returns>The string representation of the member's value.</returns>
+ internal string GetValue(IMessage message) {
+ try {
+ object value = this.GetValueAsObject(message);
+ return this.ToString(value, false);
+ } catch (FormatException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name);
+ }
+ }
+
+ /// <summary>
+ /// Gets the value of a member of a given message.
+ /// Used in serialization.
+ /// </summary>
+ /// <param name="message">The message instance to read the value from.</param>
+ /// <param name="originalValue">A value indicating whether the original value should be retrieved (as opposed to a normalized form of it).</param>
+ /// <returns>The string representation of the member's value.</returns>
+ internal string GetValue(IMessage message, bool originalValue) {
+ try {
+ object value = this.GetValueAsObject(message);
+ return this.ToString(value, originalValue);
+ } catch (FormatException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name);
+ }
+ }
+
+ /// <summary>
+ /// Gets whether the value has been set to something other than its CLR type default value.
+ /// </summary>
+ /// <param name="message">The message instance to check the value on.</param>
+ /// <returns>True if the value is not the CLR default value.</returns>
+ internal bool IsNondefaultValueSet(IMessage message) {
+ if (this.memberDeclaredType.IsValueType) {
+ return !this.GetValueAsObject(message).Equals(this.defaultMemberValue);
+ } else {
+ return this.defaultMemberValue != this.GetValueAsObject(message);
+ }
+ }
+
+ /// <summary>
+ /// Figures out the CLR default value for a given type.
+ /// </summary>
+ /// <param name="type">The type whose default value is being sought.</param>
+ /// <returns>Either null, or some default value like 0 or 0.0.</returns>
+ private static object DeriveDefaultValue(Type type) {
+ if (type.IsValueType) {
+ return Activator.CreateInstance(type);
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Checks whether a type is a nullable value type (i.e. int?)
+ /// </summary>
+ /// <param name="type">The type in question.</param>
+ /// <returns>True if this is a nullable value type.</returns>
+ private static bool IsNonNullableValueType(Type type) {
+ if (!type.IsValueType) {
+ return false;
+ }
+
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Retrieves a previously instantiated encoder of a given type, or creates a new one and stores it for later retrieval as well.
+ /// </summary>
+ /// <param name="messagePartEncoder">The message part encoder type.</param>
+ /// <returns>An instance of the desired encoder.</returns>
+ private static IMessagePartEncoder GetEncoder(Type messagePartEncoder) {
+ Requires.NotNull(messagePartEncoder, "messagePartEncoder");
+ Contract.Ensures(Contract.Result<IMessagePartEncoder>() != null);
+
+ IMessagePartEncoder encoder;
+ if (!encoders.TryGetValue(messagePartEncoder, out encoder)) {
+ try {
+ encoder = encoders[messagePartEncoder] = (IMessagePartEncoder)Activator.CreateInstance(messagePartEncoder);
+ } catch (MissingMethodException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.EncoderInstantiationFailed, messagePartEncoder.FullName);
+ }
+ }
+
+ return encoder;
+ }
+
+ /// <summary>
+ /// Gets the value of the message part, without converting it to/from a string.
+ /// </summary>
+ /// <param name="message">The message instance to read from.</param>
+ /// <returns>The value of the member.</returns>
+ private object GetValueAsObject(IMessage message) {
+ if (this.property != null) {
+ return this.property.GetValue(message, null);
+ } else {
+ return this.field.GetValue(message);
+ }
+ }
+
+ /// <summary>
+ /// Sets the value of a message part directly with a given value.
+ /// </summary>
+ /// <param name="message">The message instance to read from.</param>
+ /// <param name="value">The value to set on the this part.</param>
+ private void SetValueAsObject(IMessage message, object value) {
+ if (this.property != null) {
+ this.property.SetValue(message, value, null);
+ } else {
+ this.field.SetValue(message, value);
+ }
+ }
+
+ /// <summary>
+ /// Converts a string representation of the member's value to the appropriate type.
+ /// </summary>
+ /// <param name="value">The string representation of the member's value.</param>
+ /// <returns>
+ /// An instance of the appropriate type for setting the member.
+ /// </returns>
+ private object ToValue(string value) {
+ return this.converter.StringToValue(value);
+ }
+
+ /// <summary>
+ /// Converts the member's value to its string representation.
+ /// </summary>
+ /// <param name="value">The value of the member.</param>
+ /// <param name="originalString">A value indicating whether a string matching the originally decoded string should be returned (as opposed to a normalized string).</param>
+ /// <returns>
+ /// The string representation of the member's value.
+ /// </returns>
+ private string ToString(object value, bool originalString) {
+ return originalString ? this.converter.ValueToOriginalString(value) : this.converter.ValueToString(value);
+ }
+
+ /// <summary>
+ /// Validates that the message part and its attribute have agreeable settings.
+ /// </summary>
+ /// <exception cref="ArgumentException">
+ /// Thrown when a non-nullable value type is set as optional.
+ /// </exception>
+ private void ValidateSettings() {
+ if (!this.IsRequired && IsNonNullableValueType(this.memberDeclaredType)) {
+ MemberInfo member = (MemberInfo)this.field ?? this.property;
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ "Invalid combination: {0} on message type {1} is a non-nullable value type but is marked as optional.",
+ member.Name,
+ member.DeclaringType));
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs
new file mode 100644
index 0000000..9c0fa83
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs
@@ -0,0 +1,67 @@
+//-----------------------------------------------------------------------
+// <copyright file="ValueMapping.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Reflection {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// A pair of conversion functions to map some type to a string and back again.
+ /// </summary>
+ [ContractVerification(true)]
+ internal struct ValueMapping {
+ /// <summary>
+ /// The mapping function that converts some custom type to a string.
+ /// </summary>
+ internal readonly Func<object, string> ValueToString;
+
+ /// <summary>
+ /// The mapping function that converts some custom type to the original string
+ /// (possibly non-normalized) that represents it.
+ /// </summary>
+ internal readonly Func<object, string> ValueToOriginalString;
+
+ /// <summary>
+ /// The mapping function that converts a string to some custom type.
+ /// </summary>
+ internal readonly Func<string, object> StringToValue;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ValueMapping"/> struct.
+ /// </summary>
+ /// <param name="toString">The mapping function that converts some custom value to a string.</param>
+ /// <param name="toOriginalString">The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the <paramref name="toString"/> function.</param>
+ /// <param name="toValue">The mapping function that converts a string to some custom value.</param>
+ internal ValueMapping(Func<object, string> toString, Func<object, string> toOriginalString, Func<string, object> toValue) {
+ Requires.NotNull(toString, "toString");
+ Requires.NotNull(toValue, "toValue");
+
+ this.ValueToString = toString;
+ this.ValueToOriginalString = toOriginalString ?? toString;
+ this.StringToValue = toValue;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ValueMapping"/> struct.
+ /// </summary>
+ /// <param name="encoder">The encoder.</param>
+ internal ValueMapping(IMessagePartEncoder encoder) {
+ Requires.NotNull(encoder, "encoder");
+ var nullEncoder = encoder as IMessagePartNullEncoder;
+ string nullString = nullEncoder != null ? nullEncoder.EncodedNullValue : null;
+
+ var originalStringEncoder = encoder as IMessagePartOriginalEncoder;
+ Func<object, string> originalStringEncode = encoder.Encode;
+ if (originalStringEncoder != null) {
+ originalStringEncode = originalStringEncoder.EncodeAsOriginalString;
+ }
+
+ this.ValueToString = obj => (obj != null) ? encoder.Encode(obj) : nullString;
+ this.StringToValue = str => (str != null) ? encoder.Decode(str) : null;
+ this.ValueToOriginalString = obj => (obj != null) ? originalStringEncode(obj) : nullString;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactory.cs b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactory.cs
new file mode 100644
index 0000000..5db206e
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactory.cs
@@ -0,0 +1,298 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardMessageFactory.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Reflection;
+ using System.Text;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A message factory that automatically selects the message type based on the incoming data.
+ /// </summary>
+ internal class StandardMessageFactory : IMessageFactory {
+ /// <summary>
+ /// The request message types and their constructors to use for instantiating the messages.
+ /// </summary>
+ private readonly Dictionary<MessageDescription, ConstructorInfo> requestMessageTypes = new Dictionary<MessageDescription, ConstructorInfo>();
+
+ /// <summary>
+ /// The response message types and their constructors to use for instantiating the messages.
+ /// </summary>
+ /// <value>
+ /// The value is a dictionary, whose key is the type of the constructor's lone parameter.
+ /// </value>
+ private readonly Dictionary<MessageDescription, Dictionary<Type, ConstructorInfo>> responseMessageTypes = new Dictionary<MessageDescription, Dictionary<Type, ConstructorInfo>>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardMessageFactory"/> class.
+ /// </summary>
+ internal StandardMessageFactory() {
+ }
+
+ /// <summary>
+ /// Adds message types to the set that this factory can create.
+ /// </summary>
+ /// <param name="messageTypes">The message types that this factory may instantiate.</param>
+ public virtual void AddMessageTypes(IEnumerable<MessageDescription> messageTypes) {
+ Requires.NotNull(messageTypes, "messageTypes");
+ Requires.True(messageTypes.All(msg => msg != null), "messageTypes");
+
+ var unsupportedMessageTypes = new List<MessageDescription>(0);
+ foreach (MessageDescription messageDescription in messageTypes) {
+ bool supportedMessageType = false;
+
+ // First see whether this message fits the recognized pattern for request messages.
+ if (typeof(IDirectedProtocolMessage).IsAssignableFrom(messageDescription.MessageType)) {
+ foreach (ConstructorInfo ctor in messageDescription.Constructors) {
+ ParameterInfo[] parameters = ctor.GetParameters();
+ if (parameters.Length == 2 && parameters[0].ParameterType == typeof(Uri) && parameters[1].ParameterType == typeof(Version)) {
+ supportedMessageType = true;
+ this.requestMessageTypes.Add(messageDescription, ctor);
+ break;
+ }
+ }
+ }
+
+ // Also see if this message fits the recognized pattern for response messages.
+ if (typeof(IDirectResponseProtocolMessage).IsAssignableFrom(messageDescription.MessageType)) {
+ var responseCtors = new Dictionary<Type, ConstructorInfo>(messageDescription.Constructors.Length);
+ foreach (ConstructorInfo ctor in messageDescription.Constructors) {
+ ParameterInfo[] parameters = ctor.GetParameters();
+ if (parameters.Length == 1 && typeof(IDirectedProtocolMessage).IsAssignableFrom(parameters[0].ParameterType)) {
+ responseCtors.Add(parameters[0].ParameterType, ctor);
+ }
+ }
+
+ if (responseCtors.Count > 0) {
+ supportedMessageType = true;
+ this.responseMessageTypes.Add(messageDescription, responseCtors);
+ }
+ }
+
+ if (!supportedMessageType) {
+ unsupportedMessageTypes.Add(messageDescription);
+ }
+ }
+
+ ErrorUtilities.VerifySupported(
+ !unsupportedMessageTypes.Any(),
+ MessagingStrings.StandardMessageFactoryUnsupportedMessageType,
+ unsupportedMessageTypes.ToStringDeferred());
+ }
+
+ #region IMessageFactory Members
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="recipient">The intended or actual recipient of the request message.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ public virtual IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) {
+ MessageDescription matchingType = this.GetMessageDescription(recipient, fields);
+ if (matchingType != null) {
+ return this.InstantiateAsRequest(matchingType, recipient);
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Analyzes an incoming request message payload to discover what kind of
+ /// message is embedded in it and returns the type, or null if no match is found.
+ /// </summary>
+ /// <param name="request">The message that was sent as a request that resulted in the response.</param>
+ /// <param name="fields">The name/value pairs that make up the message payload.</param>
+ /// <returns>
+ /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can
+ /// deserialize to. Null if the request isn't recognized as a valid protocol message.
+ /// </returns>
+ public virtual IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) {
+ MessageDescription matchingType = this.GetMessageDescription(request, fields);
+ if (matchingType != null) {
+ return this.InstantiateAsResponse(matchingType, request);
+ } else {
+ return null;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the message type that best fits the given incoming request data.
+ /// </summary>
+ /// <param name="recipient">The recipient of the incoming data. Typically not used, but included just in case.</param>
+ /// <param name="fields">The data of the incoming message.</param>
+ /// <returns>
+ /// The message type that matches the incoming data; or <c>null</c> if no match.
+ /// </returns>
+ /// <exception cref="ProtocolException">May be thrown if the incoming data is ambiguous.</exception>
+ protected virtual MessageDescription GetMessageDescription(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) {
+ Requires.NotNull(recipient, "recipient");
+ Requires.NotNull(fields, "fields");
+
+ var matches = this.requestMessageTypes.Keys
+ .Where(message => message.CheckMessagePartsPassBasicValidation(fields))
+ .OrderByDescending(message => CountInCommon(message.Mapping.Keys, fields.Keys))
+ .ThenByDescending(message => message.Mapping.Count)
+ .CacheGeneratedResults();
+ var match = matches.FirstOrDefault();
+ if (match != null) {
+ if (Logger.Messaging.IsWarnEnabled && matches.Count() > 1) {
+ Logger.Messaging.WarnFormat(
+ "Multiple message types seemed to fit the incoming data: {0}",
+ matches.ToStringDeferred());
+ }
+
+ return match;
+ } else {
+ // No message type matches the incoming data.
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the message type that best fits the given incoming direct response data.
+ /// </summary>
+ /// <param name="request">The request message that prompted the response data.</param>
+ /// <param name="fields">The data of the incoming message.</param>
+ /// <returns>
+ /// The message type that matches the incoming data; or <c>null</c> if no match.
+ /// </returns>
+ /// <exception cref="ProtocolException">May be thrown if the incoming data is ambiguous.</exception>
+ protected virtual MessageDescription GetMessageDescription(IDirectedProtocolMessage request, IDictionary<string, string> fields) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(fields, "fields");
+
+ var matches = (from responseMessageType in this.responseMessageTypes
+ let message = responseMessageType.Key
+ where message.CheckMessagePartsPassBasicValidation(fields)
+ let ctors = this.FindMatchingResponseConstructors(message, request.GetType())
+ where ctors.Any()
+ orderby GetDerivationDistance(ctors.First().GetParameters()[0].ParameterType, request.GetType()),
+ CountInCommon(message.Mapping.Keys, fields.Keys) descending,
+ message.Mapping.Count descending
+ select message).CacheGeneratedResults();
+ var match = matches.FirstOrDefault();
+ if (match != null) {
+ if (Logger.Messaging.IsWarnEnabled && matches.Count() > 1) {
+ Logger.Messaging.WarnFormat(
+ "Multiple message types seemed to fit the incoming data: {0}",
+ matches.ToStringDeferred());
+ }
+
+ return match;
+ } else {
+ // No message type matches the incoming data.
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Instantiates the given request message type.
+ /// </summary>
+ /// <param name="messageDescription">The message description.</param>
+ /// <param name="recipient">The recipient.</param>
+ /// <returns>The instantiated message. Never null.</returns>
+ protected virtual IDirectedProtocolMessage InstantiateAsRequest(MessageDescription messageDescription, MessageReceivingEndpoint recipient) {
+ Requires.NotNull(messageDescription, "messageDescription");
+ Requires.NotNull(recipient, "recipient");
+ Contract.Ensures(Contract.Result<IDirectedProtocolMessage>() != null);
+
+ ConstructorInfo ctor = this.requestMessageTypes[messageDescription];
+ return (IDirectedProtocolMessage)ctor.Invoke(new object[] { recipient.Location, messageDescription.MessageVersion });
+ }
+
+ /// <summary>
+ /// Instantiates the given request message type.
+ /// </summary>
+ /// <param name="messageDescription">The message description.</param>
+ /// <param name="request">The request that resulted in this response.</param>
+ /// <returns>The instantiated message. Never null.</returns>
+ protected virtual IDirectResponseProtocolMessage InstantiateAsResponse(MessageDescription messageDescription, IDirectedProtocolMessage request) {
+ Requires.NotNull(messageDescription, "messageDescription");
+ Requires.NotNull(request, "request");
+ Contract.Ensures(Contract.Result<IDirectResponseProtocolMessage>() != null);
+
+ Type requestType = request.GetType();
+ var ctors = this.FindMatchingResponseConstructors(messageDescription, requestType);
+ ConstructorInfo ctor = null;
+ try {
+ ctor = ctors.Single();
+ } catch (InvalidOperationException) {
+ if (ctors.Any()) {
+ ErrorUtilities.ThrowInternal("More than one matching constructor for request type " + requestType.Name + " and response type " + messageDescription.MessageType.Name);
+ } else {
+ ErrorUtilities.ThrowInternal("Unexpected request message type " + requestType.FullName + " for response type " + messageDescription.MessageType.Name);
+ }
+ }
+ return (IDirectResponseProtocolMessage)ctor.Invoke(new object[] { request });
+ }
+
+ /// <summary>
+ /// Gets the hierarchical distance between a type and a type it derives from or implements.
+ /// </summary>
+ /// <param name="assignableType">The base type or interface.</param>
+ /// <param name="derivedType">The concrete class that implements the <paramref name="assignableType"/>.</param>
+ /// <returns>The distance between the two types. 0 if the types are equivalent, 1 if the type immediately derives from or implements the base type, or progressively higher integers.</returns>
+ private static int GetDerivationDistance(Type assignableType, Type derivedType) {
+ Requires.NotNull(assignableType, "assignableType");
+ Requires.NotNull(derivedType, "derivedType");
+ Requires.True(assignableType.IsAssignableFrom(derivedType), "assignableType");
+
+ // If this is the two types are equivalent...
+ if (derivedType.IsAssignableFrom(assignableType))
+ {
+ return 0;
+ }
+
+ int steps;
+ derivedType = derivedType.BaseType;
+ for (steps = 1; assignableType.IsAssignableFrom(derivedType); steps++)
+ {
+ derivedType = derivedType.BaseType;
+ }
+
+ return steps;
+ }
+
+ /// <summary>
+ /// Counts how many strings are in the intersection of two collections.
+ /// </summary>
+ /// <param name="collection1">The first collection.</param>
+ /// <param name="collection2">The second collection.</param>
+ /// <param name="comparison">The string comparison method to use.</param>
+ /// <returns>A non-negative integer no greater than the count of elements in the smallest collection.</returns>
+ private static int CountInCommon(ICollection<string> collection1, ICollection<string> collection2, StringComparison comparison = StringComparison.Ordinal) {
+ Requires.NotNull(collection1, "collection1");
+ Requires.NotNull(collection2, "collection2");
+ Contract.Ensures(Contract.Result<int>() >= 0 && Contract.Result<int>() <= Math.Min(collection1.Count, collection2.Count));
+
+ return collection1.Count(value1 => collection2.Any(value2 => string.Equals(value1, value2, comparison)));
+ }
+
+ /// <summary>
+ /// Finds constructors for response messages that take a given request message type.
+ /// </summary>
+ /// <param name="messageDescription">The message description.</param>
+ /// <param name="requestType">Type of the request message.</param>
+ /// <returns>A sequence of matching constructors.</returns>
+ private IEnumerable<ConstructorInfo> FindMatchingResponseConstructors(MessageDescription messageDescription, Type requestType) {
+ Requires.NotNull(messageDescription, "messageDescription");
+ Requires.NotNull(requestType, "requestType");
+
+ return this.responseMessageTypes[messageDescription].Where(pair => pair.Key.IsAssignableFrom(requestType)).Select(pair => pair.Value);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs
new file mode 100644
index 0000000..acfc004
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/StandardMessageFactoryChannel.cs
@@ -0,0 +1,111 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardMessageFactoryChannel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using Reflection;
+
+ /// <summary>
+ /// A channel that uses the standard message factory.
+ /// </summary>
+ public abstract class StandardMessageFactoryChannel : Channel {
+ /// <summary>
+ /// The message types receivable by this channel.
+ /// </summary>
+ private readonly ICollection<Type> messageTypes;
+
+ /// <summary>
+ /// The protocol versions supported by this channel.
+ /// </summary>
+ private readonly ICollection<Version> versions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardMessageFactoryChannel"/> class.
+ /// </summary>
+ /// <param name="messageTypes">The message types that might be encountered.</param>
+ /// <param name="versions">All the possible message versions that might be encountered.</param>
+ /// <param name="bindingElements">The binding elements to apply to the channel.</param>
+ protected StandardMessageFactoryChannel(ICollection<Type> messageTypes, ICollection<Version> versions, params IChannelBindingElement[] bindingElements)
+ : base(new StandardMessageFactory(), bindingElements) {
+ Requires.NotNull(messageTypes, "messageTypes");
+ Requires.NotNull(versions, "versions");
+
+ this.messageTypes = messageTypes;
+ this.versions = versions;
+ this.StandardMessageFactory.AddMessageTypes(GetMessageDescriptions(this.messageTypes, this.versions, this.MessageDescriptions));
+ }
+
+ /// <summary>
+ /// Gets or sets a tool that can figure out what kind of message is being received
+ /// so it can be deserialized.
+ /// </summary>
+ internal StandardMessageFactory StandardMessageFactory {
+ get { return (Messaging.StandardMessageFactory)this.MessageFactory; }
+ set { this.MessageFactory = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the message descriptions.
+ /// </summary>
+ internal sealed override MessageDescriptionCollection MessageDescriptions {
+ get {
+ return base.MessageDescriptions;
+ }
+
+ set {
+ base.MessageDescriptions = value;
+
+ // We must reinitialize the message factory so it can use the new message descriptions.
+ var factory = new StandardMessageFactory();
+ factory.AddMessageTypes(GetMessageDescriptions(this.messageTypes, this.versions, value));
+ this.MessageFactory = factory;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a tool that can figure out what kind of message is being received
+ /// so it can be deserialized.
+ /// </summary>
+ protected sealed override IMessageFactory MessageFactory {
+ get {
+ return (StandardMessageFactory)base.MessageFactory;
+ }
+
+ set {
+ StandardMessageFactory newValue = (StandardMessageFactory)value;
+ base.MessageFactory = newValue;
+ }
+ }
+
+ /// <summary>
+ /// Generates all the message descriptions for a given set of message types and versions.
+ /// </summary>
+ /// <param name="messageTypes">The message types.</param>
+ /// <param name="versions">The message versions.</param>
+ /// <param name="descriptionsCache">The cache to use when obtaining the message descriptions.</param>
+ /// <returns>The generated/retrieved message descriptions.</returns>
+ private static IEnumerable<MessageDescription> GetMessageDescriptions(ICollection<Type> messageTypes, ICollection<Version> versions, MessageDescriptionCollection descriptionsCache)
+ {
+ Requires.NotNull(messageTypes, "messageTypes");
+ Requires.NotNull(descriptionsCache, "descriptionsCache");
+ Contract.Ensures(Contract.Result<IEnumerable<MessageDescription>>() != null);
+
+ // Get all the MessageDescription objects through the standard cache,
+ // so that perhaps it will be a quick lookup, or at least it will be
+ // stored there for a quick lookup later.
+ var messageDescriptions = new List<MessageDescription>(messageTypes.Count * versions.Count);
+ messageDescriptions.AddRange(from version in versions
+ from messageType in messageTypes
+ select descriptionsCache.Get(messageType, version));
+
+ return messageDescriptions;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
new file mode 100644
index 0000000..6c6a7bb
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
@@ -0,0 +1,249 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.IO;
+ using System.Net;
+ using System.Net.Sockets;
+ using System.Reflection;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// The default handler for transmitting <see cref="HttpWebRequest"/> instances
+ /// and returning the responses.
+ /// </summary>
+ public class StandardWebRequestHandler : IDirectWebRequestHandler {
+ /// <summary>
+ /// The set of options this web request handler supports.
+ /// </summary>
+ private const DirectWebRequestOptions SupportedOptions = DirectWebRequestOptions.AcceptAllHttpResponses;
+
+ /// <summary>
+ /// The value to use for the User-Agent HTTP header.
+ /// </summary>
+ private static string userAgentValue = Assembly.GetExecutingAssembly().GetName().Name + "/" + Assembly.GetExecutingAssembly().GetName().Version;
+
+ #region IWebRequestHandler Members
+
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ public bool CanSupport(DirectWebRequestOptions options) {
+ return (options & ~SupportedOptions) == 0;
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ public Stream GetRequestStream(HttpWebRequest request) {
+ return this.GetRequestStream(request, DirectWebRequestOptions.None);
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
+ return GetRequestStreamCore(request);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ public IncomingWebResponse GetResponse(HttpWebRequest request) {
+ return this.GetResponse(request, DirectWebRequestOptions.None);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ public IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
+ // This request MAY have already been prepared by GetRequestStream, but
+ // we have no guarantee, so do it just to be safe.
+ PrepareRequest(request, false);
+
+ try {
+ Logger.Http.DebugFormat("HTTP {0} {1}", request.Method, request.RequestUri);
+ HttpWebResponse response = (HttpWebResponse)request.GetResponse();
+ return new NetworkDirectWebResponse(request.RequestUri, response);
+ } catch (WebException ex) {
+ HttpWebResponse response = (HttpWebResponse)ex.Response;
+ if (response != null && response.StatusCode == HttpStatusCode.ExpectationFailed &&
+ request.ServicePoint.Expect100Continue) {
+ // Some OpenID servers doesn't understand the Expect header and send 417 error back.
+ // If this server just failed from that, alter the ServicePoint for this server
+ // so that we don't send that header again next time (whenever that is).
+ // "Expect: 100-Continue" HTTP header. (see Google Code Issue 72)
+ // We don't want to blindly set all ServicePoints to not use the Expect header
+ // as that would be a security hole allowing any visitor to a web site change
+ // the web site's global behavior when calling that host.
+ Logger.Http.InfoFormat("HTTP POST to {0} resulted in 417 Expectation Failed. Changing ServicePoint to not use Expect: Continue next time.", request.RequestUri);
+ request.ServicePoint.Expect100Continue = false; // TODO: investigate that CAS may throw here
+
+ // An alternative to ServicePoint if we don't have permission to set that,
+ // but we'd have to set it BEFORE each request.
+ ////request.Expect = "";
+ }
+
+ if ((options & DirectWebRequestOptions.AcceptAllHttpResponses) != 0 && response != null &&
+ response.StatusCode != HttpStatusCode.ExpectationFailed) {
+ Logger.Http.InfoFormat("The HTTP error code {0} {1} is being accepted because the {2} flag is set.", (int)response.StatusCode, response.StatusCode, DirectWebRequestOptions.AcceptAllHttpResponses);
+ return new NetworkDirectWebResponse(request.RequestUri, response);
+ }
+
+ if (Logger.Http.IsErrorEnabled) {
+ if (response != null) {
+ using (var reader = new StreamReader(ex.Response.GetResponseStream())) {
+ Logger.Http.ErrorFormat("WebException from {0}: {1}{2}", ex.Response.ResponseUri, Environment.NewLine, reader.ReadToEnd());
+ }
+ } else {
+ Logger.Http.ErrorFormat("WebException {1} from {0}, no response available.", request.RequestUri, ex.Status);
+ }
+ }
+
+ // Be sure to close the response stream to conserve resources and avoid
+ // filling up all our incoming pipes and denying future requests.
+ // If in the future, some callers actually want to read this response
+ // we'll need to figure out how to reliably call Close on exception
+ // responses at all callers.
+ if (response != null) {
+ response.Close();
+ }
+
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.ErrorInRequestReplyMessage);
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether an exception was thrown because of the remote HTTP server returning HTTP 417 Expectation Failed.
+ /// </summary>
+ /// <param name="ex">The caught exception.</param>
+ /// <returns>
+ /// <c>true</c> if the failure was originally caused by a 417 Exceptation Failed error; otherwise, <c>false</c>.
+ /// </returns>
+ internal static bool IsExceptionFrom417ExpectationFailed(Exception ex) {
+ while (ex != null) {
+ WebException webEx = ex as WebException;
+ if (webEx != null) {
+ HttpWebResponse response = webEx.Response as HttpWebResponse;
+ if (response != null) {
+ if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
+ return true;
+ }
+ }
+ }
+
+ ex = ex.InnerException;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Initiates a POST request and prepares for sending data.
+ /// </summary>
+ /// <param name="request">The HTTP request with information about the remote party to contact.</param>
+ /// <returns>
+ /// The stream where the POST entity can be written.
+ /// </returns>
+ private static Stream GetRequestStreamCore(HttpWebRequest request) {
+ PrepareRequest(request, true);
+
+ try {
+ return request.GetRequestStream();
+ } catch (SocketException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.WebRequestFailed, request.RequestUri);
+ } catch (WebException ex) {
+ throw ErrorUtilities.Wrap(ex, MessagingStrings.WebRequestFailed, request.RequestUri);
+ }
+ }
+
+ /// <summary>
+ /// Prepares an HTTP request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="preparingPost"><c>true</c> if this is a POST request whose headers have not yet been sent out; <c>false</c> otherwise.</param>
+ private static void PrepareRequest(HttpWebRequest request, bool preparingPost) {
+ Requires.NotNull(request, "request");
+
+ // Be careful to not try to change the HTTP headers that have already gone out.
+ if (preparingPost || request.Method == "GET") {
+ // Set/override a few properties of the request to apply our policies for requests.
+ if (Debugger.IsAttached) {
+ // Since a debugger is attached, requests may be MUCH slower,
+ // so give ourselves huge timeouts.
+ request.ReadWriteTimeout = (int)TimeSpan.FromHours(1).TotalMilliseconds;
+ request.Timeout = (int)TimeSpan.FromHours(1).TotalMilliseconds;
+ }
+
+ // Some sites, such as Technorati, return 403 Forbidden on identity
+ // pages unless a User-Agent header is included.
+ if (string.IsNullOrEmpty(request.UserAgent)) {
+ request.UserAgent = userAgentValue;
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs
new file mode 100644
index 0000000..b28e5a8
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs
@@ -0,0 +1,55 @@
+//-----------------------------------------------------------------------
+// <copyright file="TimespanSecondsEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// Encodes and decodes the <see cref="TimeSpan"/> as an integer of total seconds.
+ /// </summary>
+ internal class TimespanSecondsEncoder : IMessagePartEncoder {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TimespanSecondsEncoder"/> class.
+ /// </summary>
+ public TimespanSecondsEncoder() {
+ // Note that this constructor is public so it can be instantiated via Activator.
+ }
+
+ #region IMessagePartEncoder Members
+
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>
+ /// The <paramref name="value"/> in string form, ready for message transport.
+ /// </returns>
+ public string Encode(object value) {
+ TimeSpan? timeSpan = value as TimeSpan?;
+ if (timeSpan.HasValue) {
+ return timeSpan.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>
+ /// The deserialized form of the given string.
+ /// </returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ public object Decode(string value) {
+ return TimeSpan.FromSeconds(double.Parse(value, CultureInfo.InvariantCulture));
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/TimestampEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/TimestampEncoder.cs
new file mode 100644
index 0000000..b83a426
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/TimestampEncoder.cs
@@ -0,0 +1,61 @@
+//-----------------------------------------------------------------------
+// <copyright file="TimestampEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// Translates between a <see cref="DateTime"/> and the number of seconds between it and 1/1/1970 12 AM
+ /// </summary>
+ internal class TimestampEncoder : IMessagePartEncoder {
+ /// <summary>
+ /// The reference date and time for calculating time stamps.
+ /// </summary>
+ internal static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TimestampEncoder"/> class.
+ /// </summary>
+ public TimestampEncoder() {
+ }
+
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>
+ /// The <paramref name="value"/> in string form, ready for message transport.
+ /// </returns>
+ public string Encode(object value) {
+ if (value == null) {
+ return null;
+ }
+
+ var timestamp = (DateTime)value;
+ TimeSpan secondsSinceEpoch = timestamp - Epoch;
+ return ((int)secondsSinceEpoch.TotalSeconds).ToString(CultureInfo.InvariantCulture);
+ }
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>
+ /// The deserialized form of the given string.
+ /// </returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ public object Decode(string value) {
+ if (value == null) {
+ return null;
+ }
+
+ var secondsSinceEpoch = int.Parse(value, CultureInfo.InvariantCulture);
+ return Epoch.AddSeconds(secondsSinceEpoch);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/UnprotectedMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/UnprotectedMessageException.cs
new file mode 100644
index 0000000..2f21184
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/UnprotectedMessageException.cs
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------
+// <copyright file="UnprotectedMessageException.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Globalization;
+
+ /// <summary>
+ /// An exception thrown when messages cannot receive all the protections they require.
+ /// </summary>
+ [Serializable]
+ internal class UnprotectedMessageException : ProtocolException {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UnprotectedMessageException"/> class.
+ /// </summary>
+ /// <param name="faultedMessage">The message whose protection requirements could not be met.</param>
+ /// <param name="appliedProtection">The protection requirements that were fulfilled.</param>
+ internal UnprotectedMessageException(IProtocolMessage faultedMessage, MessageProtections appliedProtection)
+ : base(string.Format(CultureInfo.CurrentCulture, MessagingStrings.InsufficientMessageProtection, faultedMessage.GetType().Name, faultedMessage.RequiredProtection, appliedProtection), faultedMessage) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UnprotectedMessageException"/> class.
+ /// </summary>
+ /// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/>
+ /// that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="context">The System.Runtime.Serialization.StreamingContext
+ /// that contains contextual information about the source or destination.</param>
+ protected UnprotectedMessageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs
new file mode 100644
index 0000000..2d94130
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs
@@ -0,0 +1,476 @@
+//-----------------------------------------------------------------------
+// <copyright file="UntrustedWebRequestHandler.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Net;
+ using System.Net.Cache;
+ using System.Text.RegularExpressions;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote
+ /// server leaving dangling connections, sending too much data, causing requests against
+ /// internal servers, etc.
+ /// </summary>
+ /// <remarks>
+ /// Protections include:
+ /// * Conservative maximum time to receive the complete response.
+ /// * Only HTTP and HTTPS schemes are permitted.
+ /// * Internal IP address ranges are not permitted: 127.*.*.*, 1::*
+ /// * Internal host names are not permitted (periods must be found in the host name)
+ /// If a particular host would be permitted but is in the blacklist, it is not allowed.
+ /// If a particular host would not be permitted but is in the whitelist, it is allowed.
+ /// </remarks>
+ public class UntrustedWebRequestHandler : IDirectWebRequestHandler {
+ /// <summary>
+ /// The set of URI schemes allowed in untrusted web requests.
+ /// </summary>
+ private ICollection<string> allowableSchemes = new List<string> { "http", "https" };
+
+ /// <summary>
+ /// The collection of blacklisted hosts.
+ /// </summary>
+ private ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings);
+
+ /// <summary>
+ /// The collection of regular expressions used to identify additional blacklisted hosts.
+ /// </summary>
+ private ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs);
+
+ /// <summary>
+ /// The collection of whitelisted hosts.
+ /// </summary>
+ private ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings);
+
+ /// <summary>
+ /// The collection of regular expressions used to identify additional whitelisted hosts.
+ /// </summary>
+ private ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs);
+
+ /// <summary>
+ /// The maximum redirections to follow in the course of a single request.
+ /// </summary>
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int maximumRedirections = Configuration.MaximumRedirections;
+
+ /// <summary>
+ /// The maximum number of bytes to read from the response of an untrusted server.
+ /// </summary>
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int maximumBytesToRead = Configuration.MaximumBytesToRead;
+
+ /// <summary>
+ /// The handler that will actually send the HTTP request and collect
+ /// the response once the untrusted server gates have been satisfied.
+ /// </summary>
+ private IDirectWebRequestHandler chainedWebRequestHandler;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class.
+ /// </summary>
+ public UntrustedWebRequestHandler()
+ : this(new StandardWebRequestHandler()) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class.
+ /// </summary>
+ /// <param name="chainedWebRequestHandler">The chained web request handler.</param>
+ public UntrustedWebRequestHandler(IDirectWebRequestHandler chainedWebRequestHandler) {
+ Requires.NotNull(chainedWebRequestHandler, "chainedWebRequestHandler");
+
+ this.chainedWebRequestHandler = chainedWebRequestHandler;
+ if (Debugger.IsAttached) {
+ // Since a debugger is attached, requests may be MUCH slower,
+ // so give ourselves huge timeouts.
+ this.ReadWriteTimeout = TimeSpan.FromHours(1);
+ this.Timeout = TimeSpan.FromHours(1);
+ } else {
+ this.ReadWriteTimeout = Configuration.ReadWriteTimeout;
+ this.Timeout = Configuration.Timeout;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the default maximum bytes to read in any given HTTP request.
+ /// </summary>
+ /// <value>Default is 1MB. Cannot be less than 2KB.</value>
+ public int MaximumBytesToRead {
+ get {
+ return this.maximumBytesToRead;
+ }
+
+ set {
+ Requires.InRange(value >= 2048, "value");
+ this.maximumBytesToRead = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the total number of redirections to allow on any one request.
+ /// Default is 10.
+ /// </summary>
+ public int MaximumRedirections {
+ get {
+ return this.maximumRedirections;
+ }
+
+ set {
+ Requires.InRange(value >= 0, "value");
+ this.maximumRedirections = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the time allowed to wait for single read or write operation to complete.
+ /// Default is 500 milliseconds.
+ /// </summary>
+ public TimeSpan ReadWriteTimeout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time allowed for an entire HTTP request.
+ /// Default is 5 seconds.
+ /// </summary>
+ public TimeSpan Timeout { get; set; }
+
+ /// <summary>
+ /// Gets a collection of host name literals that should be allowed even if they don't
+ /// pass standard security checks.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")]
+ public ICollection<string> WhitelistHosts { get { return this.whitelistHosts; } }
+
+ /// <summary>
+ /// Gets a collection of host name regular expressions that indicate hosts that should
+ /// be allowed even though they don't pass standard security checks.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")]
+ public ICollection<Regex> WhitelistHostsRegex { get { return this.whitelistHostsRegex; } }
+
+ /// <summary>
+ /// Gets a collection of host name literals that should be rejected even if they
+ /// pass standard security checks.
+ /// </summary>
+ public ICollection<string> BlacklistHosts { get { return this.blacklistHosts; } }
+
+ /// <summary>
+ /// Gets a collection of host name regular expressions that indicate hosts that should
+ /// be rejected even if they pass standard security checks.
+ /// </summary>
+ public ICollection<Regex> BlacklistHostsRegex { get { return this.blacklistHostsRegex; } }
+
+ /// <summary>
+ /// Gets the configuration for this class that is specified in the host's .config file.
+ /// </summary>
+ private static UntrustedWebRequestElement Configuration {
+ get { return DotNetOpenAuthSection.Messaging.UntrustedWebRequest; }
+ }
+
+ #region IDirectWebRequestHandler Members
+
+ /// <summary>
+ /// Determines whether this instance can support the specified options.
+ /// </summary>
+ /// <param name="options">The set of options that might be given in a subsequent web request.</param>
+ /// <returns>
+ /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
+ /// </returns>
+ [Pure]
+ public bool CanSupport(DirectWebRequestOptions options) {
+ // We support whatever our chained handler supports, plus RequireSsl.
+ return this.chainedWebRequestHandler.CanSupport(options & ~DirectWebRequestOptions.RequireSsl);
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
+ /// and any other appropriate properties <i>before</i> calling this method.</para>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch.</para>
+ /// </remarks>
+ public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
+ this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0);
+
+ this.PrepareRequest(request, true);
+
+ // Submit the request and get the request stream back.
+ return this.chainedWebRequestHandler.GetRequestStream(request, options & ~DirectWebRequestOptions.RequireSsl);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <param name="options">The options to apply to this web request.</param>
+ /// <returns>
+ /// An instance of <see cref="CachedDirectWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")]
+ public IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
+ // This request MAY have already been prepared by GetRequestStream, but
+ // we have no guarantee, so do it just to be safe.
+ this.PrepareRequest(request, false);
+
+ // Since we may require SSL for every redirect, we handle each redirect manually
+ // in order to detect and fail if any redirect sends us to an HTTP url.
+ // We COULD allow automatic redirect in the cases where HTTPS is not required,
+ // but our mock request infrastructure can't do redirects on its own either.
+ Uri originalRequestUri = request.RequestUri;
+ int i;
+ for (i = 0; i < this.MaximumRedirections; i++) {
+ this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0);
+ CachedDirectWebResponse response = this.chainedWebRequestHandler.GetResponse(request, options & ~DirectWebRequestOptions.RequireSsl).GetSnapshot(this.MaximumBytesToRead);
+ if (response.Status == HttpStatusCode.MovedPermanently ||
+ response.Status == HttpStatusCode.Redirect ||
+ response.Status == HttpStatusCode.RedirectMethod ||
+ response.Status == HttpStatusCode.RedirectKeepVerb) {
+ // We have no copy of the post entity stream to repeat on our manually
+ // cloned HttpWebRequest, so we have to bail.
+ ErrorUtilities.VerifyProtocol(request.Method != "POST", MessagingStrings.UntrustedRedirectsOnPOSTNotSupported);
+ Uri redirectUri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]);
+ request = request.Clone(redirectUri);
+ } else {
+ if (response.FinalUri != request.RequestUri) {
+ // Since we don't automatically follow redirects, there's only one scenario where this
+ // can happen: when the server sends a (non-redirecting) Content-Location header in the response.
+ // It's imperative that we do not trust that header though, so coerce the FinalUri to be
+ // what we just requested.
+ Logger.Http.WarnFormat("The response from {0} included an HTTP header indicating it's the same as {1}, but it's not a redirect so we won't trust that.", request.RequestUri, response.FinalUri);
+ response.FinalUri = request.RequestUri;
+ }
+
+ return response;
+ }
+ }
+
+ throw ErrorUtilities.ThrowProtocol(MessagingStrings.TooManyRedirects, originalRequestUri);
+ }
+
+ /// <summary>
+ /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
+ /// <returns>
+ /// The writer the caller should write out the entity data to.
+ /// </returns>
+ Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request) {
+ return this.GetRequestStream(request, DirectWebRequestOptions.None);
+ }
+
+ /// <summary>
+ /// Processes an <see cref="HttpWebRequest"/> and converts the
+ /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
+ /// <returns>
+ /// An instance of <see cref="IncomingWebResponse"/> describing the response.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown for any network error.</exception>
+ /// <remarks>
+ /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
+ /// <see cref="ProtocolException"/> to abstract away the transport and provide
+ /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
+ /// value, if set, should be Closed before throwing.</para>
+ /// </remarks>
+ IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) {
+ return this.GetResponse(request, DirectWebRequestOptions.None);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1".
+ /// </summary>
+ /// <param name="ip">The ip address to check.</param>
+ /// <returns>
+ /// <c>true</c> if this is a loopback IP address; <c>false</c> otherwise.
+ /// </returns>
+ private static bool IsIPv6Loopback(IPAddress ip) {
+ Requires.NotNull(ip, "ip");
+ byte[] addressBytes = ip.GetAddressBytes();
+ for (int i = 0; i < addressBytes.Length - 1; i++) {
+ if (addressBytes[i] != 0) {
+ return false;
+ }
+ }
+ if (addressBytes[addressBytes.Length - 1] != 1) {
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Determines whether the given host name is in a host list or host name regex list.
+ /// </summary>
+ /// <param name="host">The host name.</param>
+ /// <param name="stringList">The list of host names.</param>
+ /// <param name="regexList">The list of regex patterns of host names.</param>
+ /// <returns>
+ /// <c>true</c> if the specified host falls within at least one of the given lists; otherwise, <c>false</c>.
+ /// </returns>
+ private static bool IsHostInList(string host, ICollection<string> stringList, ICollection<Regex> regexList) {
+ Requires.NotNullOrEmpty(host, "host");
+ Requires.NotNull(stringList, "stringList");
+ Requires.NotNull(regexList, "regexList");
+ foreach (string testHost in stringList) {
+ if (string.Equals(host, testHost, StringComparison.OrdinalIgnoreCase)) {
+ return true;
+ }
+ }
+ foreach (Regex regex in regexList) {
+ if (regex.IsMatch(host)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether a given host is whitelisted.
+ /// </summary>
+ /// <param name="host">The host name to test.</param>
+ /// <returns>
+ /// <c>true</c> if the host is whitelisted; otherwise, <c>false</c>.
+ /// </returns>
+ private bool IsHostWhitelisted(string host) {
+ return IsHostInList(host, this.WhitelistHosts, this.WhitelistHostsRegex);
+ }
+
+ /// <summary>
+ /// Determines whether a given host is blacklisted.
+ /// </summary>
+ /// <param name="host">The host name to test.</param>
+ /// <returns>
+ /// <c>true</c> if the host is blacklisted; otherwise, <c>false</c>.
+ /// </returns>
+ private bool IsHostBlacklisted(string host) {
+ return IsHostInList(host, this.BlacklistHosts, this.BlacklistHostsRegex);
+ }
+
+ /// <summary>
+ /// Verify that the request qualifies under our security policies
+ /// </summary>
+ /// <param name="requestUri">The request URI.</param>
+ /// <param name="requireSsl">If set to <c>true</c>, only web requests that can be made entirely over SSL will succeed.</param>
+ /// <exception cref="ProtocolException">Thrown when the URI is disallowed for security reasons.</exception>
+ private void EnsureAllowableRequestUri(Uri requestUri, bool requireSsl) {
+ ErrorUtilities.VerifyProtocol(this.IsUriAllowable(requestUri), MessagingStrings.UnsafeWebRequestDetected, requestUri);
+ ErrorUtilities.VerifyProtocol(!requireSsl || String.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase), MessagingStrings.InsecureWebRequestWithSslRequired, requestUri);
+ }
+
+ /// <summary>
+ /// Determines whether a URI is allowed based on scheme and host name.
+ /// No requireSSL check is done here
+ /// </summary>
+ /// <param name="uri">The URI to test for whether it should be allowed.</param>
+ /// <returns>
+ /// <c>true</c> if [is URI allowable] [the specified URI]; otherwise, <c>false</c>.
+ /// </returns>
+ private bool IsUriAllowable(Uri uri) {
+ Requires.NotNull(uri, "uri");
+ if (!this.allowableSchemes.Contains(uri.Scheme)) {
+ Logger.Http.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
+ return false;
+ }
+
+ // Allow for whitelist or blacklist to override our detection.
+ Func<string, bool> failsUnlessWhitelisted = (string reason) => {
+ if (IsHostWhitelisted(uri.DnsSafeHost)) {
+ return true;
+ }
+ Logger.Http.WarnFormat("Rejecting URL {0} because {1}.", uri, reason);
+ return false;
+ };
+
+ // Try to interpret the hostname as an IP address so we can test for internal
+ // IP address ranges. Note that IP addresses can appear in many forms
+ // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1
+ // So we convert them to a canonical IPAddress instance, and test for all
+ // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1
+ // Note that Uri.IsLoopback is very unreliable, not catching many of these variants.
+ IPAddress hostIPAddress;
+ if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress)) {
+ byte[] addressBytes = hostIPAddress.GetAddressBytes();
+
+ // The host is actually an IP address.
+ switch (hostIPAddress.AddressFamily) {
+ case System.Net.Sockets.AddressFamily.InterNetwork:
+ if (addressBytes[0] == 127 || addressBytes[0] == 10) {
+ return failsUnlessWhitelisted("it is a loopback address.");
+ }
+ break;
+ case System.Net.Sockets.AddressFamily.InterNetworkV6:
+ if (IsIPv6Loopback(hostIPAddress)) {
+ return failsUnlessWhitelisted("it is a loopback address.");
+ }
+ break;
+ default:
+ return failsUnlessWhitelisted("it does not use an IPv4 or IPv6 address.");
+ }
+ } else {
+ // The host is given by name. We require names to contain periods to
+ // help make sure it's not an internal address.
+ if (!uri.Host.Contains(".")) {
+ return failsUnlessWhitelisted("it does not contain a period in the host name.");
+ }
+ }
+ if (this.IsHostBlacklisted(uri.DnsSafeHost)) {
+ Logger.Http.WarnFormat("Rejected URL {0} because it is blacklisted.", uri);
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Prepares the request by setting timeout and redirect policies.
+ /// </summary>
+ /// <param name="request">The request to prepare.</param>
+ /// <param name="preparingPost"><c>true</c> if this is a POST request whose headers have not yet been sent out; <c>false</c> otherwise.</param>
+ private void PrepareRequest(HttpWebRequest request, bool preparingPost) {
+ Requires.NotNull(request, "request");
+
+ // Be careful to not try to change the HTTP headers that have already gone out.
+ if (preparingPost || request.Method == "GET") {
+ // Set/override a few properties of the request to apply our policies for untrusted requests.
+ request.ReadWriteTimeout = (int)this.ReadWriteTimeout.TotalMilliseconds;
+ request.Timeout = (int)this.Timeout.TotalMilliseconds;
+ request.KeepAlive = false;
+ }
+
+ // If SSL is required throughout, we cannot allow auto redirects because
+ // it may include a pass through an unprotected HTTP request.
+ // We have to follow redirects manually.
+ // It also allows us to ignore HttpWebResponse.FinalUri since that can be affected by
+ // the Content-Location header and open security holes.
+ request.AllowAutoRedirect = false;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs
new file mode 100644
index 0000000..2c653d0
--- /dev/null
+++ b/src/DotNetOpenAuth.Core/Messaging/UriStyleMessageFormatter.cs
@@ -0,0 +1,77 @@
+//-----------------------------------------------------------------------
+// <copyright file="UriStyleMessageFormatter.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A serializer for <see cref="DataBag"/>-derived types
+ /// </summary>
+ /// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam>
+ internal class UriStyleMessageFormatter<T> : DataBagFormatterBase<T> where T : DataBag, new() {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriStyleMessageFormatter&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="signingKey">The crypto service provider with the asymmetric key to use for signing or verifying the token.</param>
+ /// <param name="encryptingKey">The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected internal UriStyleMessageFormatter(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(signingKey, encryptingKey, compressed, maximumAge, decodeOnceOnly) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriStyleMessageFormatter&lt;T&gt;"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
+ /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
+ /// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
+ /// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
+ /// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
+ /// <param name="minimumAge">The minimum age.</param>
+ /// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
+ /// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
+ protected internal UriStyleMessageFormatter(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
+ : base(cryptoKeyStore, bucket, signed, encrypted, compressed, minimumAge, maximumAge, decodeOnceOnly) {
+ Requires.True((cryptoKeyStore != null && !String.IsNullOrEmpty(bucket)) || (!signed && !encrypted), null);
+ }
+
+ /// <summary>
+ /// Serializes the <see cref="DataBag"/> instance to a buffer.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>The buffer containing the serialized data.</returns>
+ protected override byte[] SerializeCore(T message) {
+ var fields = MessageSerializer.Get(message.GetType()).Serialize(MessageDescriptions.GetAccessor(message));
+ string value = MessagingUtilities.CreateQueryString(fields);
+ return Encoding.UTF8.GetBytes(value);
+ }
+
+ /// <summary>
+ /// Deserializes the <see cref="DataBag"/> instance from a buffer.
+ /// </summary>
+ /// <param name="message">The message instance to initialize with data from the buffer.</param>
+ /// <param name="data">The data buffer.</param>
+ protected override void DeserializeCore(T message, byte[] data) {
+ string value = Encoding.UTF8.GetString(data);
+
+ // Deserialize into message newly created instance.
+ var serializer = MessageSerializer.Get(message.GetType());
+ var fields = MessageDescriptions.GetAccessor(message);
+ serializer.Deserialize(HttpUtility.ParseQueryString(value).ToDictionary(), fields);
+ }
+ }
+}