diff options
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging')
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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> types and returns a caching <see cref="IEnumerator<T>"/> + /// from its <see cref="IEnumerable<T>.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<T>"/> 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<T>"/> 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<T>"/> 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 -> 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'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'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 '{0}' with value '{1}'.. + /// </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 '{0}' 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 '{0}' parameter '{1}' with value '{2}'.. + /// </summary> + internal static string MessagePartReadFailure { + get { + return ResourceManager.GetString("MessagePartReadFailure", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Message parameter '{0}' with value '{1}' 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 '{0}' parameter '{1}' 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'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'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 '{0}'.. + /// </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 '{0}' had unexpected value '{1}'.. + /// </summary> + internal static string UnexpectedMessagePartValue { + get { + return ResourceManager.GetString("UnexpectedMessagePartValue", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Expected message {0} parameter '{1}' to have value '{2}' but had '{3}' 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 '{0}' 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 '{0}' 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 '{0}' is unrecognized and unsupported.. + /// </summary> + internal static string UnsupportedHttpVerb { + get { + return ResourceManager.GetString("UnsupportedHttpVerb", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to '{0}' messages cannot be received with HTTP verb '{1}'.. + /// </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 '{0}' 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 & 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&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 & 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 & 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<string, string>. + /// </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<string, string></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<string, string>. + /// </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<TElement> 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<T>"/> into an <see cref="IComparer<T>"/>. + /// </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<string, string> 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<string, string> 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<T>"/> 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<T>"/> 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); + } + } +} |