diff options
Diffstat (limited to 'src/DotNetOpenAuth.Core')
118 files changed, 20490 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Core/Assumes.cs b/src/DotNetOpenAuth.Core/Assumes.cs new file mode 100644 index 0000000..67205a2 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Assumes.cs @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------- +// <copyright file="Assumes.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// Internal state consistency checks that throw an internal error exception when they fail. + /// </summary> + internal static class Assumes { + /// <summary> + /// Validates some expression describing the acceptable condition evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an internal error exception.</param> + /// <param name="message">The message to include with the exception.</param> + [Pure, DebuggerStepThrough] + internal static void True(bool condition, string message = null) { + if (!condition) { + Fail(message); + } + } + + /// <summary> + /// Validates some expression describing the acceptable condition evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an internal error exception.</param> + /// <param name="unformattedMessage">The unformatted message.</param> + /// <param name="args">Formatting arguments.</param> + [Pure, DebuggerStepThrough] + internal static void True(bool condition, string unformattedMessage, params object[] args) { + if (!condition) { + Fail(String.Format(unformattedMessage, args)); + } + } + + /// <summary> + /// Throws an internal error exception. + /// </summary> + /// <param name="message">The message.</param> + [Pure, DebuggerStepThrough] + internal static void Fail(string message = null) { + if (message != null) { + throw new InternalErrorException(message); + } else { + throw new InternalErrorException(); + } + } + + /// <summary> + /// An internal error exception that should never be caught. + /// </summary> + [Serializable] + private class InternalErrorException : Exception { + /// <summary> + /// Initializes a new instance of the <see cref="InternalErrorException"/> class. + /// </summary> + internal InternalErrorException() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="InternalErrorException"/> class. + /// </summary> + /// <param name="message">The message.</param> + internal 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> + internal 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/CodeAnalysisDictionary.xml b/src/DotNetOpenAuth.Core/CodeAnalysisDictionary.xml new file mode 100644 index 0000000..8c90df3 --- /dev/null +++ b/src/DotNetOpenAuth.Core/CodeAnalysisDictionary.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8" ?> +<Dictionary> + <Words> + <!-- + This is a list of case-insensitive words that exist in the dictionary + but you do not want to be recognized by IdentifiersShouldBeSpelledCorrectly. + Do not add deprecated terms to this list, instead add these to the + <Deprecated> section below. + --> + <Unrecognized> + <!--<Word>cb</Word>--> + </Unrecognized> + <!-- + This is a list of case-insensitive words that do not exist in the dictionary + but you still want to be considered as recognized by + IdentifiersShouldBeSpelledCorrectly. Do not add compound words (e.g. 'FileName') + to this list as this will cause CompoundWordsShouldBeBeCasedCorrectly to fire on + usages of the compound word stating that they should be changed to their discrete equivalent + (for example 'FileName' -> 'Filename'). + --> + <Recognized> + <Word>OAuth</Word> + <!--<Word>cryptoKeyStore</Word> + <Word>containingMessage</Word> + <Word>httpRequestInfo</Word> + <Word>faultedMessage</Word> + <Word>keyStore</Word> + <Word>authorizationServer</Word> + <Word>bytesToSign</Word> + <Word>clientCallback</Word>--> + </Recognized> + <Deprecated> + <!-- + This is a list of deprecated terms with their preferred alternates and is + used by UsePreferredTerms. The deprecated terms are case-insensitive, + however, make sure to pascal-case the preferred alternates. If a word + does not have a preferred alternate, simply leave it blank. + --> + <!--<Term PreferredAlternate="EnterpriseServices">complus</Term>--> + </Deprecated> + <Compound> + <!-- + This is a list of discrete terms with their compound alternates and is used by + CompoundWordsShouldBeCasedCorrectly. These are words that exist in the + dictionary as discrete terms, however, should actually be cased as compound words. + For example, 'Filename' exists in the dictionary and hence the spelling rules will + not see it as unrecognized but its actual preferred usage is 'FileName'; adding it + below causes CompoundWordsShouldBeCasedCorrectly to fire. The discrete terms are + case-insensitive, however, be sure to pascal-case the compound alternates. + Any discrete terms added below automatically get added to the list of discrete + exceptions to prevent CompoundWordsShouldBeCasedCorrectly from firing both on the + compound word (for example 'WhiteSpace') and its discrete alternate (for example + 'Whitespace'). + --> + <Term CompoundAlternate="OAuth">oauth</Term> + <!--<Term CompoundAlternate="DataBind">databind</Term>--> + </Compound> + <DiscreteExceptions> + <!-- + This is a list of case-insensitive exceptions to the CompoundWordsShouldBeCasedCorrectly + discrete term check. As this check works solely on the basis of whether two consecutive + tokens exists in the dictionary, it can have a high false positive rate. For example, + 'onset' exists in the dictionary but the user probably intended it to be 'OnSet'. + Adding this word below prevents this rule from firing telling the user to change 'OnSet' + to 'Onset'. + --> + <Term>oauth</Term> + <!--<Term>onset</Term>--> + </DiscreteExceptions> + </Words> +</Dictionary>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd new file mode 100644 index 0000000..d193776 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd @@ -0,0 +1,968 @@ +<?xml version="1.0" encoding="utf-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:vs="http://schemas.microsoft.com/Visual-Studio-Intellisense" + elementFormDefault="qualified" + attributeFormDefault="unqualified"> + <xs:element name="dotNetOpenAuth"> + <xs:annotation> + <xs:documentation> + Customizations and configuration of DotNetOpenAuth behavior. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="messaging"> + <xs:annotation> + <xs:documentation> + Options for general messaging protocols, such as whitelist/blacklist hosts and maximum message age. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="untrustedWebRequest"> + <xs:annotation> + <xs:documentation> + Restrictions and settings to apply to outgoing HTTP requests to hosts that are not + trusted by this web site. Useful for OpenID-supporting hosts because HTTP connections + are initiated based on user input to arbitrary servers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="whitelistHosts"> + <xs:annotation> + <xs:documentation> + A set of host names (including domain names) to allow outgoing connections to + that would otherwise not be allowed based on security restrictions. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The host name to trust. For example: "localhost" or "www.mypartners.com". + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The host name to NOT trust. For example: "localhost" or "www.mypartners.com". + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:annotation> + <xs:documentation> + Clears all hosts from the whitelist. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="whitelistHostsRegex"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="blacklistHosts"> + <xs:annotation> + <xs:documentation> + A set of host names (including domain names) to disallow outgoing connections to + that would otherwise be allowed based on security restrictions. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The host name known to add to the blacklist. For example: "localhost" or "www.mypartners.com". + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The host name known to remove to the blacklist. For example: "localhost" or "www.mypartners.com". + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:annotation> + <xs:documentation> + Clears all hosts from the blacklist. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="blacklistHostsRegex"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="timeout" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time to allow for an outgoing HTTP request to complete before giving up. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="readWriteTimeout" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time to allow for an outgoing HTTP request to either send or receive data before giving up. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="maximumBytesToRead" type="xs:int"> + <xs:annotation> + <xs:documentation> + The maximum bytes to read from an untrusted server during an outgoing HTTP request before cutting off the response. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="maximumRedirections" type="xs:int"> + <xs:annotation> + <xs:documentation> + The maximum redirection instructions to follow before giving up. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="webResourceUrlProvider"> + <xs:annotation> + <xs:documentation> + The type that implements the DotNetOpenAuth.IEmbeddedResourceRetrieval interface + to instantiate for obtaining URLs that fetch embedded resource streams. + Primarily useful when the System.Web.UI.Page class is not used in the ASP.NET pipeline. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements the IEmbeddedResourceRetrieval interface. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="lifetime" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time allowed between a message being sent to when it is received before + it is considered expired. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="clockSkew" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time to consider a safe difference in server clocks. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="strict" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether remote parties will be held strictly to the protocol specifications. + Strict will require that remote parties adhere strictly to the specifications, + even when a loose interpretation would not compromise security. + true is a good default because it shakes out interoperability bugs in remote services + so they can be identified and corrected. But some web sites want things to Just Work + more than they want to file bugs against others, so false is the setting for them. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="relaxSslRequirements" type="xs:boolean" default="false"> + <xs:annotation> + <xs:documentation> + Whether SSL requirements within the library are disabled/relaxed. + Use for TESTING ONLY. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="maximumIndirectMessageUrlLength" type="xs:int" default="2048"> + <xs:annotation> + <xs:documentation> + 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. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="privateSecretMaximumAge" type="xs:string" default="28.00:00:00"> + <xs:annotation> + <xs:documentation> + The maximum age of a secret used for private signing or encryption before it is renewed. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="openid"> + <xs:annotation> + <xs:documentation> + Configuration for OpenID authentication (relying parties and providers). + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="relyingParty"> + <xs:annotation> + <xs:documentation> + Configuration specific for OpenID relying parties. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="security"> + <xs:annotation> + <xs:documentation> + Security settings that apply to OpenID relying parties. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="trustedProviders"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="endpoint" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The OpenID Provider Endpoint (aka "OP Endpoint") that this relying party trusts. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="endpoint" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="rejectAssertionsFromUntrustedProviders" type="xs:boolean" default="false"> + <xs:annotation> + <xs:documentation> + A value indicating whether any login attempt coming from an OpenID Provider Endpoint that is not on this + whitelist of trusted OP Endpoints will be rejected. If the trusted providers list is empty and this value + is true, all assertions are rejected. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="requireSsl" type="xs:boolean" default="false"> + <xs:annotation> + <xs:documentation> + Restricts OpenID logins to identifiers that use HTTPS throughout the discovery process, + and only uses HTTPS OpenID Provider endpoints. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="minimumRequiredOpenIdVersion"> + <xs:annotation> + <xs:documentation> + Optionally restricts interoperability with remote parties that + implement older versions of OpenID. + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:NMTOKEN"> + <xs:enumeration value="V10" /> + <xs:enumeration value="V11" /> + <xs:enumeration value="V20" /> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="minimumHashBitLength" type="xs:int"> + <xs:annotation> + <xs:documentation> + Shared associations with OpenID Providers will only be formed or used if they + are willing to form associations equal to or greater than a given level of protection. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="maximumHashBitLength" type="xs:int"> + <xs:annotation> + <xs:documentation> + Shared associaitons with OpenID Providers will only be formed or used if they + are willing to form associations equal to or less than a given level of protection. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="requireDirectedIdentity" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Requires that OpenID identifiers upon which authentication requests are created + are to be OP Identifiers. Claimed Identifiers are not allowed. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="requireAssociation" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Requires that the relying party can form a shared association with an + OpenID Provider before creating an authentication request for it. + Note that this does not require that the Provider actually use a + shared association in its response. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="rejectUnsolicitedAssertions" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Requires that users begin their login experience at the relying party + rather than at a Provider or using other forms of unsolicited assertions. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="rejectDelegatingIdentifiers" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Requires that the claimed identifiers used to log into the relying party + be the same ones that are originally issued by the Provider. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="ignoreUnsignedExtensions" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Makes it impossible for the relying party to read authentication response + extensions that are not signed by the Provider. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether identifiers that are both OP Identifiers and Claimed Identifiers + should ever be recognized as claimed identifiers. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="allowApproximateIdentifierDiscovery" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether certain Claimed Identifiers that exploit + features that .NET does not have the ability to send exact HTTP requests for will + still be allowed by using an approximate HTTP request. + Only impacts hosts running under partial trust. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether the relying party should take special care + to protect users against replay attacks when interoperating with OpenID 1.1 Providers. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="behaviors"> + <xs:annotation> + <xs:documentation> + Manipulates the set of custom behaviors that are automatically applied + to incoming and outgoing OpenID messages. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements the IRelyingPartyBehavior interface. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements the IRelyingPartyBehavior interface. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="discoveryServices"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="store"> + <xs:annotation> + <xs:documentation> + A custom implementation of IRelyingPartyApplicationStore to use by default for new + instances of OpenIdRelyingParty. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="type" type="xs:string"> + <xs:annotation> + <xs:documentation> + A fully-qualified type name of the custom implementation of IRelyingPartyApplicationStore. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="preserveUserSuppliedIdentifier" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether "dnoa.userSuppliedIdentifier" is tacked onto the openid.return_to URL in order to preserve what the user typed into the OpenID box. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="provider"> + <xs:annotation> + <xs:documentation> + Configuration specific for OpenID providers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="security"> + <xs:annotation> + <xs:documentation> + Security settings that apply to OpenID providers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="associations"> + <xs:annotation> + <xs:documentation> + Sets maximum ages for shared associations of various strengths. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The OpenID association type (i.e. HMAC-SHA1 or HMAC-SHA256) + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="lifetime" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The lifetime a shared association of this type will be used for. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The OpenID association type (i.e. HMAC-SHA1 or HMAC-SHA256) + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="requireSsl" type="xs:boolean" default="false"> + <xs:annotation> + <xs:documentation> + Requires that relying parties' realm URLs be protected by HTTPS, + ensuring that the RP discovery step is not vulnerable to DNS poisoning attacks. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Provides automatic security protections to OpenID 1.x relying parties + so security is comparable to OpenID 2.0 relying parties. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="encodeAssociationSecretsInHandles" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether the Provider should ease the burden of storing associations + by encoding their secrets (in signed, encrypted form) into the association handles themselves, storing only + a few rotating, private symmetric keys in the Provider's store instead. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="unsolicitedAssertionVerification"> + <xs:annotation> + <xs:documentation> + The level of verification done on a claimed identifier before an unsolicited + assertion for that identifier is issued by this Provider. + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:NMTOKEN"> + <xs:enumeration value="RequireSuccess"> + <xs:annotation> + <xs:documentation> + The claimed identifier being asserted must delegate to this Provider + and this must be verifiable by the Provider to send the assertion. + </xs:documentation> + </xs:annotation> + </xs:enumeration> + <xs:enumeration value="LogWarningOnFailure"> + <xs:annotation> + <xs:documentation> + The claimed identifier being asserted is checked for delegation to this Provider + and an warning is logged, but the assertion is allowed to go through. + </xs:documentation> + </xs:annotation> + </xs:enumeration> + <xs:enumeration value="NeverVerify"> + <xs:annotation> + <xs:documentation> + The claimed identifier being asserted is not checked to see that this Provider + has authority to assert its identity. + </xs:documentation> + </xs:annotation> + </xs:enumeration> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="minimumHashBitLength" type="xs:int"> + <xs:annotation> + <xs:documentation> + The minimum shared association strength to form with relying parties. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="maximumHashBitLength" type="xs:int"> + <xs:annotation> + <xs:documentation> + The maximum shared association strength to form with relying parties. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="behaviors"> + <xs:annotation> + <xs:documentation> + Manipulates the set of custom behaviors that are automatically applied + to incoming and outgoing OpenID messages. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements the IRelyingPartyBehavior interface. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="store"> + <xs:annotation> + <xs:documentation> + A custom implementation of IProviderApplicationStore to use by default for new + instances of OpenIdRelyingParty. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="type" type="xs:string"> + <xs:annotation> + <xs:documentation> + A fully-qualified type name of the custom implementation of IProviderApplicationStore. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="extensionFactories"> + <xs:annotation> + <xs:documentation> + Adjusts the list of known OpenID extensions via the registration of extension factories. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements IOpenIdExtensionFactory. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements IOpenIdExtensionFactory. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="xriResolver"> + <xs:annotation> + <xs:documentation> + Controls XRI resolution to XRDS documents. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="enabled" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether XRI identifiers are allowed at all. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="proxy" type="xs:string"> + <xs:annotation> + <xs:documentation> + The XRI proxy resolver to use for obtaining XRDS documents from an XRI. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + <xs:attribute name="maxAuthenticationTime" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time a user can take at the Provider while logging in before a relying party considers + the authentication lost. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="cacheDiscovery" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Whether the results of identifier discovery should be cached for a short time to improve performance + on subsequent requests, at the potential risk of reading stale data. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="oauth"> + <xs:annotation> + <xs:documentation> + Settings for OAuth consumers and service providers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="consumer"> + <xs:annotation> + <xs:documentation> + Settings applicable to OAuth Consumers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="security"> + <xs:annotation> + <xs:documentation> + Security settings applicable to OAuth Consumers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="serviceProvider"> + <xs:annotation> + <xs:documentation> + Settings applicable to OAuth Service Providers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="security"> + <xs:annotation> + <xs:documentation> + Security settings applicable to OAuth Service Providers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="minimumRequiredOAuthVersion" default="V10"> + <xs:annotation> + <xs:documentation> + Optionally restricts interoperability with OAuth consumers that implement + older versions of OAuth. + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:NMTOKEN"> + <xs:enumeration value="V10"> + <xs:annotation> + <xs:documentation> + The initial version of OAuth, now known to be vulnerable to certain social engineering attacks. + </xs:documentation> + </xs:annotation> + </xs:enumeration> + <xs:enumeration value="V10a"> + <xs:annotation> + <xs:documentation> + The OAuth version that protects against social engineering attacks by introducing + the oauth_verifier parameter. + </xs:documentation> + </xs:annotation> + </xs:enumeration> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="maxAuthorizationTime" type="xs:string" default="0:05"> + <xs:annotation> + <xs:documentation> + The maximum time allowed for users to authorize a consumer before request tokens expire. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="store"> + <xs:annotation> + <xs:documentation> + Sets the custom type that implements the INonceStore interface to use for nonce checking. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="type" type="xs:string"> + <xs:annotation> + <xs:documentation> + A fully-qualified type name of the custom implementation of INonceStore. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="reporting"> + <xs:annotation> + <xs:documentation> + Adjusts statistical reports DotNetOpenAuth may send to the library authors to + assist with future development of the library. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:attribute name="enabled" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether reporting is active at all or entirely inactive. + Note that even if active, the reports may be more or less empty based + on other settings. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="minimumReportingInterval" type="xs:string"> + <xs:annotation> + <xs:documentation> + Controls how frequently reports are collected and transmitted. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="minimumFlushInterval" type="xs:string"> + <xs:annotation> + <xs:documentation> + Controls how frequently the statistics that are collected in memory are persisted to disk. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="includeFeatureUsage" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether a list of features in DotNetOpenAuth that are actually used by this host + are included in the report. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="includeEventStatistics" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether a set of counters that track how often certain events (such as an + successful or failed authentication) is included in the report. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="includeLocalRequestUris" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether to include a few of this host's URLs that contain DotNetOpenAuth components. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="includeCultures" type="xs:boolean" default="true"> + <xs:annotation> + <xs:documentation> + Whether to include the cultures as set on the user agents of incoming requests to pages + that contain DotNetOpenAuth components. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuthSection.cs b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuthSection.cs new file mode 100644 index 0000000..e0c7fc4 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuthSection.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="DotNetOpenAuthSection.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.Web; + using System.Web.Configuration; + + /// <summary> + /// Represents the section in the host's .config file that configures + /// this library's settings. + /// </summary> + [ContractVerification(true)] + public class DotNetOpenAuthSection : ConfigurationSectionGroup { + /// <summary> + /// The name of the section under which this library's settings must be found. + /// </summary> + internal const string SectionName = "dotNetOpenAuth"; + + /// <summary> + /// The name of the <openid> sub-element. + /// </summary> + private const string OpenIdElementName = "openid"; + + /// <summary> + /// The name of the <oauth> sub-element. + /// </summary> + private const string OAuthElementName = "oauth"; + + /// <summary> + /// Initializes a new instance of the <see cref="DotNetOpenAuthSection"/> class. + /// </summary> + internal DotNetOpenAuthSection() { + } + + /// <summary> + /// Gets the messaging configuration element. + /// </summary> + public static MessagingElement Messaging { + get { return MessagingElement.Configuration; } + } + + /// <summary> + /// Gets the reporting configuration element. + /// </summary> + internal static ReportingElement Reporting { + get { return ReportingElement.Configuration; } + } + + /// <summary> + /// Gets a named section in this section group, or <c>null</c> if no such section is defined. + /// </summary> + /// <param name="name">The name of the section to obtain.</param> + /// <returns>The desired section, or null if it could not be obtained.</returns> + internal static ConfigurationSection GetNamedSection(string name) { + string fullyQualifiedSectionName = SectionName + "/" + name; + if (HttpContext.Current != null) { + return (ConfigurationSection)WebConfigurationManager.GetSection(fullyQualifiedSectionName); + } else { + var configuration = ConfigurationManager.OpenExeConfiguration(null); + return configuration != null ? configuration.GetSection(fullyQualifiedSectionName) : null; + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/HostNameElement.cs b/src/DotNetOpenAuth.Core/Configuration/HostNameElement.cs new file mode 100644 index 0000000..9df218e --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/HostNameElement.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------- +// <copyright file="HostNameElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Represents the name of a single host or a regex pattern for host names. + /// </summary> + [ContractVerification(true)] + internal class HostNameElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @name attribute. + /// </summary> + private const string NameConfigName = "name"; + + /// <summary> + /// Initializes a new instance of the <see cref="HostNameElement"/> class. + /// </summary> + internal HostNameElement() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="HostNameElement"/> class. + /// </summary> + /// <param name="name">The default value of the <see cref="Name"/> property.</param> + internal HostNameElement(string name) { + this.Name = name; + } + + /// <summary> + /// Gets or sets the name of the host on the white or black list. + /// </summary> + [ConfigurationProperty(NameConfigName, IsRequired = true, IsKey = true)] + ////[StringValidator(MinLength = 1)] + public string Name { + get { return (string)this[NameConfigName]; } + set { this[NameConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/HostNameOrRegexCollection.cs b/src/DotNetOpenAuth.Core/Configuration/HostNameOrRegexCollection.cs new file mode 100644 index 0000000..c7d963b --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/HostNameOrRegexCollection.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="HostNameOrRegexCollection.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.Text.RegularExpressions; + + /// <summary> + /// Represents a collection of child elements that describe host names either as literal host names or regex patterns. + /// </summary> + [ContractVerification(true)] + internal class HostNameOrRegexCollection : ConfigurationElementCollection { + /// <summary> + /// Initializes a new instance of the <see cref="HostNameOrRegexCollection"/> class. + /// </summary> + public HostNameOrRegexCollection() { + } + + /// <summary> + /// Gets all the members of the collection assuming they are all literal host names. + /// </summary> + internal IEnumerable<string> KeysAsStrings { + get { + foreach (HostNameElement element in this) { + yield return element.Name; + } + } + } + + /// <summary> + /// Gets all the members of the collection assuming they are all host names regex patterns. + /// </summary> + internal IEnumerable<Regex> KeysAsRegexs { + get { + foreach (HostNameElement element in this) { + if (element.Name != null) { + yield return new Regex(element.Name); + } + } + } + } + + /// <summary> + /// Creates a new child host name element. + /// </summary> + /// <returns> + /// A new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override ConfigurationElement CreateNewElement() { + return new HostNameElement(); + } + + /// <summary> + /// Gets the element key for a specified configuration element. + /// </summary> + /// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement"/> to return the key for.</param> + /// <returns> + /// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override object GetElementKey(ConfigurationElement element) { + Contract.Assume(element != null); // this should be Contract.Requires in base class. + return ((HostNameElement)element).Name ?? string.Empty; + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/MessagingElement.cs b/src/DotNetOpenAuth.Core/Configuration/MessagingElement.cs new file mode 100644 index 0000000..7c3e242 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/MessagingElement.cs @@ -0,0 +1,209 @@ +//----------------------------------------------------------------------- +// <copyright file="MessagingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// Represents the <messaging> element in the host's .config file. + /// </summary> + [ContractVerification(true)] + public class MessagingElement : ConfigurationSection { + /// <summary> + /// The name of the <webResourceUrlProvider> sub-element. + /// </summary> + private const string WebResourceUrlProviderName = "webResourceUrlProvider"; + + /// <summary> + /// The name of the <untrustedWebRequest> sub-element. + /// </summary> + private const string UntrustedWebRequestElementName = "untrustedWebRequest"; + + /// <summary> + /// The name of the attribute that stores the association's maximum lifetime. + /// </summary> + private const string MaximumMessageLifetimeConfigName = "lifetime"; + + /// <summary> + /// The name of the attribute that stores the maximum allowable clock skew. + /// </summary> + private const string MaximumClockSkewConfigName = "clockSkew"; + + /// <summary> + /// The name of the attribute that indicates whether to disable SSL requirements across the library. + /// </summary> + private const string RelaxSslRequirementsConfigName = "relaxSslRequirements"; + + /// <summary> + /// The name of the attribute that controls whether messaging rules are strictly followed. + /// </summary> + private const string StrictConfigName = "strict"; + + /// <summary> + /// The default value for the <see cref="MaximumIndirectMessageUrlLength"/> property. + /// </summary> + /// <value> + /// 2KB, recommended by OpenID group + /// </value> + private const int DefaultMaximumIndirectMessageUrlLength = 2 * 1024; + + /// <summary> + /// The name of the attribute that controls the maximum length of a URL before it is converted + /// to a POST payload. + /// </summary> + private const string MaximumIndirectMessageUrlLengthConfigName = "maximumIndirectMessageUrlLength"; + + /// <summary> + /// Gets the name of the @privateSecretMaximumAge attribute. + /// </summary> + private const string PrivateSecretMaximumAgeConfigName = "privateSecretMaximumAge"; + + /// <summary> + /// The name of the <messaging> sub-element. + /// </summary> + private const string MessagingElementName = DotNetOpenAuthSection.SectionName + "/messaging"; + + /// <summary> + /// Gets the configuration section from the .config file. + /// </summary> + public static MessagingElement Configuration { + get { + Contract.Ensures(Contract.Result<MessagingElement>() != null); + return (MessagingElement)ConfigurationManager.GetSection(MessagingElementName) ?? new MessagingElement(); + } + } + + /// <summary> + /// Gets the actual maximum message lifetime that a program should allow. + /// </summary> + /// <value>The sum of the <see cref="MaximumMessageLifetime"/> and + /// <see cref="MaximumClockSkew"/> property values.</value> + public TimeSpan MaximumMessageLifetime { + get { return this.MaximumMessageLifetimeNoSkew + this.MaximumClockSkew; } + } + + /// <summary> + /// Gets or sets the maximum lifetime of a private symmetric secret, + /// that may be used for signing or encryption. + /// </summary> + /// <value>The default value is 28 days (twice the age of the longest association).</value> + [ConfigurationProperty(PrivateSecretMaximumAgeConfigName, DefaultValue = "28.00:00:00")] + public TimeSpan PrivateSecretMaximumAge { + get { return (TimeSpan)this[PrivateSecretMaximumAgeConfigName]; } + set { this[PrivateSecretMaximumAgeConfigName] = value; } + } + + /// <summary> + /// Gets or sets the time between a message's creation and its receipt + /// before it is considered expired. + /// </summary> + /// <value> + /// The default value value is 3 minutes. + /// </value> + /// <remarks> + /// <para>Smaller timespans mean lower tolerance for delays in message delivery. + /// Larger timespans mean more nonces must be stored to provide replay protection.</para> + /// <para>The maximum age a message implementing the + /// <see cref="IExpiringProtocolMessage"/> interface can be before + /// being discarded as too old.</para> + /// <para>This time limit should NOT take into account expected + /// time skew for servers across the Internet. Time skew is added to + /// this value and is controlled by the <see cref="MaximumClockSkew"/> property.</para> + /// </remarks> + [ConfigurationProperty(MaximumMessageLifetimeConfigName, DefaultValue = "00:03:00")] + internal TimeSpan MaximumMessageLifetimeNoSkew { + get { return (TimeSpan)this[MaximumMessageLifetimeConfigName]; } + set { this[MaximumMessageLifetimeConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum clock skew. + /// </summary> + /// <value>The default value is 10 minutes.</value> + /// <remarks> + /// <para>Smaller timespans mean lower tolerance for + /// time variance due to server clocks not being synchronized. + /// Larger timespans mean greater chance for replay attacks and + /// larger nonce caches.</para> + /// <para>For example, if a server could conceivably have its + /// clock d = 5 minutes off UTC time, then any two servers could have + /// their clocks disagree by as much as 2*d = 10 minutes. </para> + /// </remarks> + [ConfigurationProperty(MaximumClockSkewConfigName, DefaultValue = "00:10:00")] + internal TimeSpan MaximumClockSkew { + get { return (TimeSpan)this[MaximumClockSkewConfigName]; } + set { this[MaximumClockSkewConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether SSL requirements within the library are disabled/relaxed. + /// Use for TESTING ONLY. + /// </summary> + [ConfigurationProperty(RelaxSslRequirementsConfigName, DefaultValue = false)] + internal bool RelaxSslRequirements { + get { return (bool)this[RelaxSslRequirementsConfigName]; } + set { this[RelaxSslRequirementsConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether messaging rules are strictly + /// adhered to. + /// </summary> + /// <value><c>true</c> by default.</value> + /// <remarks> + /// Strict will require that remote parties adhere strictly to the specifications, + /// even when a loose interpretation would not compromise security. + /// <c>true</c> is a good default because it shakes out interoperability bugs in remote services + /// so they can be identified and corrected. But some web sites want things to Just Work + /// more than they want to file bugs against others, so <c>false</c> is the setting for them. + /// </remarks> + [ConfigurationProperty(StrictConfigName, DefaultValue = true)] + internal bool Strict { + get { return (bool)this[StrictConfigName]; } + set { this[StrictConfigName] = value; } + } + + /// <summary> + /// Gets or sets the configuration for the <see cref="UntrustedWebRequestHandler"/> class. + /// </summary> + /// <value>The untrusted web request.</value> + [ConfigurationProperty(UntrustedWebRequestElementName)] + internal UntrustedWebRequestElement UntrustedWebRequest { + get { return (UntrustedWebRequestElement)this[UntrustedWebRequestElementName] ?? new UntrustedWebRequestElement(); } + set { this[UntrustedWebRequestElementName] = value; } + } + + /// <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> + [ConfigurationProperty(MaximumIndirectMessageUrlLengthConfigName, DefaultValue = DefaultMaximumIndirectMessageUrlLength)] + [IntegerValidator(MinValue = 500, MaxValue = 4096)] + internal int MaximumIndirectMessageUrlLength { + get { return (int)this[MaximumIndirectMessageUrlLengthConfigName]; } + set { this[MaximumIndirectMessageUrlLengthConfigName] = value; } + } + + /// <summary> + /// Gets or sets the embedded resource retrieval provider. + /// </summary> + /// <value> + /// The embedded resource retrieval provider. + /// </value> + [ConfigurationProperty(WebResourceUrlProviderName)] + internal TypeConfigurationElement<IEmbeddedResourceRetrieval> EmbeddedResourceRetrievalProvider { + get { return (TypeConfigurationElement<IEmbeddedResourceRetrieval>)this[WebResourceUrlProviderName] ?? new TypeConfigurationElement<IEmbeddedResourceRetrieval>(); } + set { this[WebResourceUrlProviderName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/ReportingElement.cs b/src/DotNetOpenAuth.Core/Configuration/ReportingElement.cs new file mode 100644 index 0000000..a8eb7d3 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/ReportingElement.cs @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------- +// <copyright file="ReportingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// Represents the <reporting> element in the host's .config file. + /// </summary> + internal class ReportingElement : ConfigurationSection { + /// <summary> + /// The name of the @enabled attribute. + /// </summary> + private const string EnabledAttributeName = "enabled"; + + /// <summary> + /// The name of the @minimumReportingInterval attribute. + /// </summary> + private const string MinimumReportingIntervalAttributeName = "minimumReportingInterval"; + + /// <summary> + /// The name of the @minimumFlushInterval attribute. + /// </summary> + private const string MinimumFlushIntervalAttributeName = "minimumFlushInterval"; + + /// <summary> + /// The name of the @includeFeatureUsage attribute. + /// </summary> + private const string IncludeFeatureUsageAttributeName = "includeFeatureUsage"; + + /// <summary> + /// The name of the @includeEventStatistics attribute. + /// </summary> + private const string IncludeEventStatisticsAttributeName = "includeEventStatistics"; + + /// <summary> + /// The name of the @includeLocalRequestUris attribute. + /// </summary> + private const string IncludeLocalRequestUrisAttributeName = "includeLocalRequestUris"; + + /// <summary> + /// The name of the @includeCultures attribute. + /// </summary> + private const string IncludeCulturesAttributeName = "includeCultures"; + + /// <summary> + /// The name of the <reporting> sub-element. + /// </summary> + private const string ReportingElementName = DotNetOpenAuthSection.SectionName + "/reporting"; + + /// <summary> + /// The default value for the @minimumFlushInterval attribute. + /// </summary> +#if DEBUG + private const string MinimumFlushIntervalDefault = "0"; +#else + private const string MinimumFlushIntervalDefault = "0:15"; +#endif + + /// <summary> + /// Initializes a new instance of the <see cref="ReportingElement"/> class. + /// </summary> + internal ReportingElement() { + } + + /// <summary> + /// Gets the configuration section from the .config file. + /// </summary> + public static ReportingElement Configuration { + get { + Contract.Ensures(Contract.Result<ReportingElement>() != null); + return (ReportingElement)ConfigurationManager.GetSection(ReportingElementName) ?? new ReportingElement(); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this reporting is enabled. + /// </summary> + /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> + [ConfigurationProperty(EnabledAttributeName, DefaultValue = true)] + internal bool Enabled { + get { return (bool)this[EnabledAttributeName]; } + set { this[EnabledAttributeName] = value; } + } + + /// <summary> + /// Gets or sets the maximum frequency that reports will be published. + /// </summary> + [ConfigurationProperty(MinimumReportingIntervalAttributeName, DefaultValue = "1")] // 1 day default + internal TimeSpan MinimumReportingInterval { + get { return (TimeSpan)this[MinimumReportingIntervalAttributeName]; } + set { this[MinimumReportingIntervalAttributeName] = value; } + } + + /// <summary> + /// Gets or sets the maximum frequency the set can be flushed to disk. + /// </summary> + [ConfigurationProperty(MinimumFlushIntervalAttributeName, DefaultValue = MinimumFlushIntervalDefault)] + internal TimeSpan MinimumFlushInterval { + get { return (TimeSpan)this[MinimumFlushIntervalAttributeName]; } + set { this[MinimumFlushIntervalAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include a list of library features used in the report. + /// </summary> + /// <value><c>true</c> to include a report of features used; otherwise, <c>false</c>.</value> + [ConfigurationProperty(IncludeFeatureUsageAttributeName, DefaultValue = true)] + internal bool IncludeFeatureUsage { + get { return (bool)this[IncludeFeatureUsageAttributeName]; } + set { this[IncludeFeatureUsageAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include statistics of certain events such as + /// authentication success and failure counting, and can include remote endpoint URIs. + /// </summary> + /// <value> + /// <c>true</c> to include event counters in the report; otherwise, <c>false</c>. + /// </value> + [ConfigurationProperty(IncludeEventStatisticsAttributeName, DefaultValue = true)] + internal bool IncludeEventStatistics { + get { return (bool)this[IncludeEventStatisticsAttributeName]; } + set { this[IncludeEventStatisticsAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include a few URLs to pages on the hosting + /// web site that host DotNetOpenAuth components. + /// </summary> + [ConfigurationProperty(IncludeLocalRequestUrisAttributeName, DefaultValue = true)] + internal bool IncludeLocalRequestUris { + get { return (bool)this[IncludeLocalRequestUrisAttributeName]; } + set { this[IncludeLocalRequestUrisAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include the cultures requested by the user agent + /// on pages that host DotNetOpenAuth components. + /// </summary> + [ConfigurationProperty(IncludeCulturesAttributeName, DefaultValue = true)] + internal bool IncludeCultures { + get { return (bool)this[IncludeCulturesAttributeName]; } + set { this[IncludeCulturesAttributeName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/TrustedProviderConfigurationCollection.cs b/src/DotNetOpenAuth.Core/Configuration/TrustedProviderConfigurationCollection.cs new file mode 100644 index 0000000..1a287fd --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/TrustedProviderConfigurationCollection.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="TrustedProviderConfigurationCollection.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + + /// <summary> + /// A configuration collection of trusted OP Endpoints. + /// </summary> + internal class TrustedProviderConfigurationCollection : ConfigurationElementCollection { + /// <summary> + /// The name of the "rejectAssertionsFromUntrustedProviders" element. + /// </summary> + private const string RejectAssertionsFromUntrustedProvidersConfigName = "rejectAssertionsFromUntrustedProviders"; + + /// <summary> + /// Initializes a new instance of the <see cref="TrustedProviderConfigurationCollection"/> class. + /// </summary> + internal TrustedProviderConfigurationCollection() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="TrustedProviderConfigurationCollection"/> class. + /// </summary> + /// <param name="elements">The elements to initialize the collection with.</param> + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Seems unavoidable")] + internal TrustedProviderConfigurationCollection(IEnumerable<TrustedProviderEndpointConfigurationElement> elements) { + Requires.NotNull(elements, "elements"); + + foreach (TrustedProviderEndpointConfigurationElement element in elements) { + this.BaseAdd(element); + } + } + + /// <summary> + /// Gets or sets a value indicating whether any login attempt coming from an OpenID Provider Endpoint that is not on this + /// whitelist of trusted OP Endpoints will be rejected. If the trusted providers list is empty and this value + /// is true, all assertions are rejected. + /// </summary> + [ConfigurationProperty(RejectAssertionsFromUntrustedProvidersConfigName, DefaultValue = false)] + internal bool RejectAssertionsFromUntrustedProviders { + get { return (bool)this[RejectAssertionsFromUntrustedProvidersConfigName]; } + set { this[RejectAssertionsFromUntrustedProvidersConfigName] = value; } + } + + /// <summary> + /// When overridden in a derived class, creates a new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </summary> + /// <returns> + /// A new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override ConfigurationElement CreateNewElement() { + return new TrustedProviderEndpointConfigurationElement(); + } + + /// <summary> + /// Gets the element key for a specified configuration element when overridden in a derived class. + /// </summary> + /// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement"/> to return the key for.</param> + /// <returns> + /// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override object GetElementKey(ConfigurationElement element) { + return ((TrustedProviderEndpointConfigurationElement)element).ProviderEndpoint; + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/TrustedProviderEndpointConfigurationElement.cs b/src/DotNetOpenAuth.Core/Configuration/TrustedProviderEndpointConfigurationElement.cs new file mode 100644 index 0000000..2576eb0 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/TrustedProviderEndpointConfigurationElement.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// <copyright file="TrustedProviderEndpointConfigurationElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + + /// <summary> + /// A configuration element that records a trusted Provider Endpoint. + /// </summary> + internal class TrustedProviderEndpointConfigurationElement : ConfigurationElement { + /// <summary> + /// The name of the attribute that stores the <see cref="ProviderEndpoint"/> value. + /// </summary> + private const string ProviderEndpointConfigName = "endpoint"; + + /// <summary> + /// Initializes a new instance of the <see cref="TrustedProviderEndpointConfigurationElement"/> class. + /// </summary> + public TrustedProviderEndpointConfigurationElement() { + } + + /// <summary> + /// Gets or sets the OpenID Provider Endpoint (aka "OP Endpoint") that this relying party trusts. + /// </summary> + [ConfigurationProperty(ProviderEndpointConfigName, IsRequired = true, IsKey = true)] + public Uri ProviderEndpoint { + get { return (Uri)this[ProviderEndpointConfigName]; } + set { this[ProviderEndpointConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationCollection.cs b/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationCollection.cs new file mode 100644 index 0000000..95b9c50 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationCollection.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// <copyright file="TypeConfigurationCollection.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A collection of <see cref="TypeConfigurationElement<T>"/>. + /// </summary> + /// <typeparam name="T">The type that all types specified in the elements must derive from.</typeparam> + [ContractVerification(true)] + internal class TypeConfigurationCollection<T> : ConfigurationElementCollection + where T : class { + /// <summary> + /// Initializes a new instance of the TypeConfigurationCollection class. + /// </summary> + internal TypeConfigurationCollection() { + } + + /// <summary> + /// Initializes a new instance of the TypeConfigurationCollection class. + /// </summary> + /// <param name="elements">The elements that should be added to the collection initially.</param> + internal TypeConfigurationCollection(IEnumerable<Type> elements) { + Requires.NotNull(elements, "elements"); + + foreach (Type element in elements) { + this.BaseAdd(new TypeConfigurationElement<T> { TypeName = element.AssemblyQualifiedName }); + } + } + + /// <summary> + /// Creates instances of all the types listed in the collection. + /// </summary> + /// <param name="allowInternals">if set to <c>true</c> then internal types may be instantiated.</param> + /// <returns>A sequence of instances generated from types in this collection. May be empty, but never null.</returns> + internal IEnumerable<T> CreateInstances(bool allowInternals) { + Contract.Ensures(Contract.Result<IEnumerable<T>>() != null); + return from element in this.Cast<TypeConfigurationElement<T>>() + where !element.IsEmpty + select element.CreateInstance(default(T), allowInternals); + } + + /// <summary> + /// When overridden in a derived class, creates a new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </summary> + /// <returns> + /// A new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override ConfigurationElement CreateNewElement() { + return new TypeConfigurationElement<T>(); + } + + /// <summary> + /// Gets the element key for a specified configuration element when overridden in a derived class. + /// </summary> + /// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement"/> to return the key for.</param> + /// <returns> + /// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override object GetElementKey(ConfigurationElement element) { + Contract.Assume(element != null); // this should be Contract.Requires in base class. + TypeConfigurationElement<T> typedElement = (TypeConfigurationElement<T>)element; + return (!string.IsNullOrEmpty(typedElement.TypeName) ? typedElement.TypeName : typedElement.XamlSource) ?? string.Empty; + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationElement.cs b/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationElement.cs new file mode 100644 index 0000000..fb1dee0 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/TypeConfigurationElement.cs @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------- +// <copyright file="TypeConfigurationElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.IO; + using System.Reflection; + using System.Web; +#if CLR4 + using System.Xaml; +#else + using System.Windows.Markup; +#endif + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Represents an element in a .config file that allows the user to provide a @type attribute specifying + /// the full type that provides some service used by this library. + /// </summary> + /// <typeparam name="T">A constraint on the type the user may provide.</typeparam> + internal class TypeConfigurationElement<T> : ConfigurationElement + where T : class { + /// <summary> + /// The name of the attribute whose value is the full name of the type the user is specifying. + /// </summary> + private const string CustomTypeConfigName = "type"; + + /// <summary> + /// The name of the attribute whose value is the path to the XAML file to deserialize to obtain the type. + /// </summary> + private const string XamlReaderSourceConfigName = "xaml"; + + /// <summary> + /// Initializes a new instance of the TypeConfigurationElement class. + /// </summary> + public TypeConfigurationElement() { + } + + /// <summary> + /// Gets or sets the full name of the type. + /// </summary> + /// <value>The full name of the type, such as: "ConsumerPortal.Code.CustomStore, ConsumerPortal".</value> + [ConfigurationProperty(CustomTypeConfigName)] + ////[SubclassTypeValidator(typeof(T))] // this attribute is broken in .NET, I think. + public string TypeName { + get { return (string)this[CustomTypeConfigName]; } + set { this[CustomTypeConfigName] = value; } + } + + /// <summary> + /// Gets or sets the path to the XAML file to deserialize to obtain the instance. + /// </summary> + [ConfigurationProperty(XamlReaderSourceConfigName)] + public string XamlSource { + get { return (string)this[XamlReaderSourceConfigName]; } + set { this[XamlReaderSourceConfigName] = value; } + } + + /// <summary> + /// Gets the type described in the .config file. + /// </summary> + public Type CustomType { + get { return string.IsNullOrEmpty(this.TypeName) ? null : Type.GetType(this.TypeName); } + } + + /// <summary> + /// Gets a value indicating whether this type has no meaningful type to instantiate. + /// </summary> + public bool IsEmpty { + get { return this.CustomType == null && string.IsNullOrEmpty(this.XamlSource); } + } + + /// <summary> + /// Creates an instance of the type described in the .config file. + /// </summary> + /// <param name="defaultValue">The value to return if no type is given in the .config file.</param> + /// <returns>The newly instantiated type.</returns> + public T CreateInstance(T defaultValue) { + Contract.Ensures(Contract.Result<T>() != null || Contract.Result<T>() == defaultValue); + + return this.CreateInstance(defaultValue, false); + } + + /// <summary> + /// Creates an instance of the type described in the .config file. + /// </summary> + /// <param name="defaultValue">The value to return if no type is given in the .config file.</param> + /// <param name="allowInternals">if set to <c>true</c> then internal types may be instantiated.</param> + /// <returns>The newly instantiated type.</returns> + public T CreateInstance(T defaultValue, bool allowInternals) { + Contract.Ensures(Contract.Result<T>() != null || Contract.Result<T>() == defaultValue); + + if (this.CustomType != null) { + if (!allowInternals) { + // Although .NET will usually prevent our instantiating non-public types, + // it will allow our instantiation of internal types within this same assembly. + // But we don't want the host site to be able to do this, so we check ourselves. + ErrorUtilities.VerifyArgument((this.CustomType.Attributes & TypeAttributes.Public) != 0, Strings.ConfigurationTypeMustBePublic, this.CustomType.FullName); + } + return (T)Activator.CreateInstance(this.CustomType); + } else if (!string.IsNullOrEmpty(this.XamlSource)) { + string source = this.XamlSource; + if (source.StartsWith("~/", StringComparison.Ordinal)) { + ErrorUtilities.VerifyHost(HttpContext.Current != null, Strings.ConfigurationXamlReferenceRequiresHttpContext, this.XamlSource); + source = HttpContext.Current.Server.MapPath(source); + } + using (Stream xamlFile = File.OpenRead(source)) { + return CreateInstanceFromXaml(xamlFile); + } + } else { + return defaultValue; + } + } + + /// <summary> + /// Creates the instance from xaml. + /// </summary> + /// <param name="xaml">The stream of xaml to deserialize.</param> + /// <returns>The deserialized object.</returns> + /// <remarks> + /// This exists as its own method to prevent the CLR's JIT compiler from failing + /// to compile the CreateInstance method just because the PresentationFramework.dll + /// may be missing (which it is on some shared web hosts). This way, if the + /// XamlSource attribute is never used, the PresentationFramework.dll never need + /// be present. + /// </remarks> + private static T CreateInstanceFromXaml(Stream xaml) { + Contract.Ensures(Contract.Result<T>() != null); +#if CLR4 + return (T)XamlServices.Load(xaml); +#else + return (T)XamlReader.Load(xaml); +#endif + } + } +} diff --git a/src/DotNetOpenAuth.Core/Configuration/UntrustedWebRequestElement.cs b/src/DotNetOpenAuth.Core/Configuration/UntrustedWebRequestElement.cs new file mode 100644 index 0000000..43e41d9 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Configuration/UntrustedWebRequestElement.cs @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------- +// <copyright file="UntrustedWebRequestElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Represents the section of a .config file where security policies regarding web requests + /// to user-provided, untrusted servers is controlled. + /// </summary> + internal class UntrustedWebRequestElement : ConfigurationElement { + #region Attribute names + + /// <summary> + /// Gets the name of the @timeout attribute. + /// </summary> + private const string TimeoutConfigName = "timeout"; + + /// <summary> + /// Gets the name of the @readWriteTimeout attribute. + /// </summary> + private const string ReadWriteTimeoutConfigName = "readWriteTimeout"; + + /// <summary> + /// Gets the name of the @maximumBytesToRead attribute. + /// </summary> + private const string MaximumBytesToReadConfigName = "maximumBytesToRead"; + + /// <summary> + /// Gets the name of the @maximumRedirections attribute. + /// </summary> + private const string MaximumRedirectionsConfigName = "maximumRedirections"; + + /// <summary> + /// Gets the name of the @whitelistHosts attribute. + /// </summary> + private const string WhitelistHostsConfigName = "whitelistHosts"; + + /// <summary> + /// Gets the name of the @whitelistHostsRegex attribute. + /// </summary> + private const string WhitelistHostsRegexConfigName = "whitelistHostsRegex"; + + /// <summary> + /// Gets the name of the @blacklistHosts attribute. + /// </summary> + private const string BlacklistHostsConfigName = "blacklistHosts"; + + /// <summary> + /// Gets the name of the @blacklistHostsRegex attribute. + /// </summary> + private const string BlacklistHostsRegexConfigName = "blacklistHostsRegex"; + + #endregion + + /// <summary> + /// Gets or sets the read/write timeout after which an HTTP request will fail. + /// </summary> + [ConfigurationProperty(ReadWriteTimeoutConfigName, DefaultValue = "00:00:01.500")] + [PositiveTimeSpanValidator] + public TimeSpan ReadWriteTimeout { + get { return (TimeSpan)this[ReadWriteTimeoutConfigName]; } + set { this[ReadWriteTimeoutConfigName] = value; } + } + + /// <summary> + /// Gets or sets the timeout after which an HTTP request will fail. + /// </summary> + [ConfigurationProperty(TimeoutConfigName, DefaultValue = "00:00:10")] + [PositiveTimeSpanValidator] + public TimeSpan Timeout { + get { return (TimeSpan)this[TimeoutConfigName]; } + set { this[TimeoutConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum bytes to read from an untrusted web server. + /// </summary> + [ConfigurationProperty(MaximumBytesToReadConfigName, DefaultValue = 1024 * 1024)] + [IntegerValidator(MinValue = 2048)] + public int MaximumBytesToRead { + get { return (int)this[MaximumBytesToReadConfigName]; } + set { this[MaximumBytesToReadConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum redirections that will be followed before an HTTP request fails. + /// </summary> + [ConfigurationProperty(MaximumRedirectionsConfigName, DefaultValue = 10)] + [IntegerValidator(MinValue = 0)] + public int MaximumRedirections { + get { return (int)this[MaximumRedirectionsConfigName]; } + set { this[MaximumRedirectionsConfigName] = value; } + } + + /// <summary> + /// Gets or sets the collection of hosts on the whitelist. + /// </summary> + [ConfigurationProperty(WhitelistHostsConfigName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(HostNameOrRegexCollection))] + public HostNameOrRegexCollection WhitelistHosts { + get { return (HostNameOrRegexCollection)this[WhitelistHostsConfigName] ?? new HostNameOrRegexCollection(); } + set { this[WhitelistHostsConfigName] = value; } + } + + /// <summary> + /// Gets or sets the collection of hosts on the blacklist. + /// </summary> + [ConfigurationProperty(BlacklistHostsConfigName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(HostNameOrRegexCollection))] + public HostNameOrRegexCollection BlacklistHosts { + get { return (HostNameOrRegexCollection)this[BlacklistHostsConfigName] ?? new HostNameOrRegexCollection(); } + set { this[BlacklistHostsConfigName] = value; } + } + + /// <summary> + /// Gets or sets the collection of regular expressions that describe hosts on the whitelist. + /// </summary> + [ConfigurationProperty(WhitelistHostsRegexConfigName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(HostNameOrRegexCollection))] + public HostNameOrRegexCollection WhitelistHostsRegex { + get { return (HostNameOrRegexCollection)this[WhitelistHostsRegexConfigName] ?? new HostNameOrRegexCollection(); } + set { this[WhitelistHostsRegexConfigName] = value; } + } + + /// <summary> + /// Gets or sets the collection of regular expressions that describe hosts on the blacklist. + /// </summary> + [ConfigurationProperty(BlacklistHostsRegexConfigName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(HostNameOrRegexCollection))] + public HostNameOrRegexCollection BlacklistHostsRegex { + get { return (HostNameOrRegexCollection)this[BlacklistHostsRegexConfigName] ?? new HostNameOrRegexCollection(); } + set { this[BlacklistHostsRegexConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj new file mode 100644 index 0000000..3692b4e --- /dev/null +++ b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))' != '' " /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <PropertyGroup> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{60426312-6AE5-4835-8667-37EDEA670222}</ProjectGuid> + <AppDesignerFolder>Properties</AppDesignerFolder> + <AssemblyName>DotNetOpenAuth.Core</AssemblyName> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.Product.props" /> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + </PropertyGroup> + <ItemGroup> + <Compile Include="Assumes.cs" /> + <Compile Include="Messaging\Bindings\AsymmetricCryptoKeyStoreWrapper.cs" /> + <Compile Include="Messaging\Bindings\CryptoKey.cs" /> + <Compile Include="Messaging\Bindings\CryptoKeyCollisionException.cs" /> + <Compile Include="Messaging\Bindings\ICryptoKeyStore.cs" /> + <Compile Include="Messaging\Bindings\MemoryCryptoKeyStore.cs" /> + <Compile Include="Messaging\BinaryDataBagFormatter.cs" /> + <Compile Include="Messaging\CachedDirectWebResponse.cs" /> + <Compile Include="Messaging\ChannelContract.cs" /> + <Compile Include="Messaging\DataBagFormatterBase.cs" /> + <Compile Include="Messaging\IHttpIndirectResponse.cs" /> + <Compile Include="Messaging\IMessageOriginalPayload.cs" /> + <Compile Include="Messaging\DirectWebRequestOptions.cs" /> + <Compile Include="Messaging\EnumerableCache.cs" /> + <Compile Include="Messaging\HostErrorException.cs" /> + <Compile Include="Messaging\IHttpDirectResponse.cs" /> + <Compile Include="Messaging\IExtensionMessage.cs" /> + <Compile Include="Messaging\IHttpDirectResponseContract.cs" /> + <Compile Include="Messaging\IMessage.cs" /> + <Compile Include="Messaging\IncomingWebResponse.cs" /> + <Compile Include="Messaging\IDirectResponseProtocolMessage.cs" /> + <Compile Include="Messaging\EmptyDictionary.cs" /> + <Compile Include="Messaging\EmptyEnumerator.cs" /> + <Compile Include="Messaging\EmptyList.cs" /> + <Compile Include="Messaging\ErrorUtilities.cs" /> + <Compile Include="Messaging\IMessageWithEvents.cs" /> + <Compile Include="Messaging\IncomingWebResponseContract.cs" /> + <Compile Include="Messaging\IProtocolMessageWithExtensions.cs" /> + <Compile Include="Messaging\InternalErrorException.cs" /> + <Compile Include="Messaging\IStreamSerializingDataBag.cs" /> + <Compile Include="Messaging\KeyedCollectionDelegate.cs" /> + <Compile Include="Messaging\MultipartPostPart.cs" /> + <Compile Include="Messaging\NetworkDirectWebResponse.cs" /> + <Compile Include="Messaging\OutgoingWebResponseActionResult.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartOriginalEncoder.cs" /> + <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> + <Compile Include="Messaging\StandardMessageFactory.cs" /> + <Compile Include="Messaging\IDataBagFormatter.cs" /> + <Compile Include="Messaging\UriStyleMessageFormatter.cs" /> + <Compile Include="Messaging\StandardMessageFactoryChannel.cs" /> + <Compile Include="Messaging\DataBag.cs" /> + <Compile Include="Messaging\TimestampEncoder.cs" /> + <Compile Include="Messaging\IMessageWithBinaryData.cs" /> + <Compile Include="Messaging\ChannelEventArgs.cs" /> + <Compile Include="Messaging\Bindings\NonceMemoryStore.cs" /> + <Compile Include="Messaging\IDirectWebRequestHandler.cs" /> + <Compile Include="Messaging\Bindings\INonceStore.cs" /> + <Compile Include="Messaging\Bindings\StandardReplayProtectionBindingElement.cs" /> + <Compile Include="Messaging\MessagePartAttribute.cs" /> + <Compile Include="Messaging\MessageProtections.cs" /> + <Compile Include="Messaging\IChannelBindingElement.cs" /> + <Compile Include="Messaging\Bindings\ReplayedMessageException.cs" /> + <Compile Include="Messaging\Bindings\ExpiredMessageException.cs" /> + <Compile Include="Messaging\Bindings\InvalidSignatureException.cs" /> + <Compile Include="Messaging\Bindings\IReplayProtectedProtocolMessage.cs" /> + <Compile Include="Messaging\Bindings\IExpiringProtocolMessage.cs" /> + <Compile Include="Messaging\Channel.cs" /> + <Compile Include="Messaging\HttpRequestInfo.cs" /> + <Compile Include="Messaging\IDirectedProtocolMessage.cs" /> + <Compile Include="Messaging\IMessageFactory.cs" /> + <Compile Include="Messaging\ITamperResistantProtocolMessage.cs" /> + <Compile Include="Messaging\MessageSerializer.cs" /> + <Compile Include="Messaging\MessagingStrings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>MessagingStrings.resx</DependentUpon> + </Compile> + <Compile Include="Messaging\MessagingUtilities.cs" /> + <Compile Include="Messaging\Bindings\StandardExpirationBindingElement.cs" /> + <Compile Include="Messaging\Reflection\ValueMapping.cs" /> + <Compile Include="Messaging\Reflection\MessageDescription.cs" /> + <Compile Include="Messaging\Reflection\MessageDictionary.cs" /> + <Compile Include="Messaging\Reflection\MessagePart.cs" /> + <Compile Include="Messaging\UnprotectedMessageException.cs" /> + <Compile Include="Messaging\OutgoingWebResponse.cs" /> + <Compile Include="Messaging\IProtocolMessage.cs" /> + <Compile Include="Messaging\HttpDeliveryMethods.cs" /> + <Compile Include="Messaging\MessageTransport.cs" /> + <Compile Include="Messaging\ProtocolException.cs" /> + <Compile Include="Messaging\TimespanSecondsEncoder.cs" /> + <Compile Include="Messaging\UntrustedWebRequestHandler.cs" /> + <Compile Include="Messaging\StandardWebRequestHandler.cs" /> + <Compile Include="Messaging\MessageReceivingEndpoint.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="Messaging\Bindings\Bindings.cd" /> + <None Include="Messaging\Exceptions.cd" /> + <None Include="Messaging\Messaging.cd" /> + </ItemGroup> + <ItemGroup> + <Compile Include="Configuration\DotNetOpenAuthSection.cs" /> + <Compile Include="Configuration\MessagingElement.cs" /> + <Compile Include="Configuration\ReportingElement.cs" /> + <Compile Include="Configuration\TrustedProviderConfigurationCollection.cs" /> + <Compile Include="Configuration\TrustedProviderEndpointConfigurationElement.cs" /> + <Compile Include="Configuration\TypeConfigurationCollection.cs" /> + <Compile Include="Configuration\TypeConfigurationElement.cs" /> + <Compile Include="Configuration\UntrustedWebRequestElement.cs" /> + <Compile Include="Configuration\HostNameOrRegexCollection.cs" /> + <Compile Include="Configuration\HostNameElement.cs" /> + <Compile Include="IEmbeddedResourceRetrieval.cs" /> + <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="Logger.cs" /> + <Compile Include="Loggers\ILog.cs" /> + <Compile Include="Loggers\Log4NetLogger.cs" /> + <Compile Include="Loggers\NoOpLogger.cs" /> + <Compile Include="Loggers\TraceLogger.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Reporting.cs" /> + <Compile Include="Requires.cs" /> + <Compile Include="Strings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>Strings.resx</DependentUpon> + </Compile> + <Compile Include="UriUtil.cs" /> + <Compile Include="Util.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="Configuration\DotNetOpenAuth.xsd" /> + <None Include="Migrated rules for DotNetOpenAuth.ruleset" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Strings.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>Strings.Designer.cs</LastGenOutput> + <SubType>Designer</SubType> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Strings.sr.resx" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Messaging\MessagingStrings.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>MessagingStrings.Designer.cs</LastGenOutput> + <SubType>Designer</SubType> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Messaging\MessagingStrings.sr.resx" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " /> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/DotNetOpenAuth.ico b/src/DotNetOpenAuth.Core/DotNetOpenAuth.ico Binary files differnew file mode 100644 index 0000000..e227dbe --- /dev/null +++ b/src/DotNetOpenAuth.Core/DotNetOpenAuth.ico diff --git a/src/DotNetOpenAuth.Core/GlobalSuppressions.cs b/src/DotNetOpenAuth.Core/GlobalSuppressions.cs new file mode 100644 index 0000000..2bc6c04 --- /dev/null +++ b/src/DotNetOpenAuth.Core/GlobalSuppressions.cs @@ -0,0 +1,70 @@ +// <auto-generated/> +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click +// "In Project Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sha", Scope = "type", Target = "DotNetOpenAuth.OAuth.ChannelElements.HmacSha1SigningBindingElement")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Hmac", Scope = "type", Target = "DotNetOpenAuth.OAuth.ChannelElements.HmacSha1SigningBindingElement")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Rsa", Scope = "type", Target = "DotNetOpenAuth.OAuth.ChannelElements.RsaSha1SigningBindingElement")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sha", Scope = "type", Target = "DotNetOpenAuth.OAuth.ChannelElements.RsaSha1SigningBindingElement")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Diffie-Hellman", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "checkInput", Scope = "member", Target = "Org.Mentalis.Security.Cryptography.DiffieHellmanManaged.#Initialize(Mono.Math.BigInteger,Mono.Math.BigInteger,Mono.Math.BigInteger,System.Int32,System.Boolean)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Org.Mentalis.Security.Cryptography.DiffieHellmanManaged.#.ctor(System.Int32,System.Int32,Org.Mentalis.Security.Cryptography.DHKeyGeneration)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Org.Mentalis.Security.Cryptography.DiffieHellmanManaged.#.ctor(System.Byte[],System.Byte[],System.Int32)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Org.Mentalis.Security.Cryptography.DiffieHellmanManaged.#.ctor(System.Byte[],System.Byte[],System.Byte[])")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Org.Mentalis.Security.Cryptography.DiffieHellman.#FromXmlString(System.String)")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "PostTrialDivisionTest", Scope = "member", Target = "Mono.Math.Prime.Generator.SequentialSearchPrimeGeneratorBase.#GenerateNewPrime(System.Int32,System.Object)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Scope = "member", Target = "Mono.Math.Prime.PrimalityTests.#GetSPPRounds(Mono.Math.BigInteger,Mono.Math.Prime.ConfidenceFactor)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Scope = "member", Target = "Mono.Math.BigInteger+ModulusRing.#BarrettReduction(Mono.Math.BigInteger)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Scope = "member", Target = "Mono.Math.BigInteger.#op_Multiply(Mono.Math.BigInteger,Mono.Math.BigInteger)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "sign", Scope = "member", Target = "Mono.Math.BigInteger.#.ctor(Mono.Math.BigInteger+Sign,System.UInt32)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "notUsed", Scope = "member", Target = "Mono.Math.BigInteger.#isProbablePrime(System.Int32)")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "returnto", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "openid", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "claimedid", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xri", Scope = "type", Target = "DotNetOpenAuth.OpenId.XriIdentifier")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "yyyy-MM-dd", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "usersetupurl", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "birthdate", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "rehydrated", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Messaging.Bindings")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.ChannelElements")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.SimpleRegistration")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Messages")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy")] +[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "We sign it when producing drops.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.OAuth")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.UI")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Messaging.Reflection")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "oauthverifier", Scope = "resource", Target = "DotNetOpenAuth.OAuth.OAuthStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "whitelist", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "icam", Scope = "resource", Target = "DotNetOpenAuth.OpenId.Behaviors.BehaviorStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "idmanagement", Scope = "resource", Target = "DotNetOpenAuth.OpenId.Behaviors.BehaviorStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "no-pii", Scope = "resource", Target = "DotNetOpenAuth.OpenId.Behaviors.BehaviorStrings.resources")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "req", Scope = "member", Target = "DotNetOpenAuth.OpenId.Provider.IAuthenticationRequestContract.#DotNetOpenAuth.OpenId.Provider.IAuthenticationRequest.ClaimedIdentifier")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "runat", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.RelyingParty.IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(DotNetOpenAuth.OpenId.RelyingParty.IAuthenticationRequest)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.RelyingParty.IRelyingPartyBehavior.OnIncomingPositiveAssertion(DotNetOpenAuth.OpenId.RelyingParty.IAuthenticationResponse)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.RelyingParty.IRelyingPartyBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.RelyingParty.RelyingPartySecuritySettings)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnOutgoingResponse(DotNetOpenAuth.OpenId.Provider.IAuthenticationRequest)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnIncomingRequest(DotNetOpenAuth.OpenId.Provider.IRequest)")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.Provider.ProviderSecuritySettings)")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Mvc", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: SuppressMessage("Microsoft.Portability", "CA1903:UseOnlyApiFromTargetedFramework", MessageId = "System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] +[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "DotNetOpenAuth.OpenId.DiffieHellmanUtilities.#.cctor()")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "DotNetOpenAuth.OAuth.Messages.SignedMessageBase.#DotNetOpenAuth.Messaging.IMessageOriginalPayload.OriginalPayload")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "iframe", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Sig", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1701:ResourceStringCompoundWordsShouldBeCasedCorrectly", MessageId = "DSig", Scope = "resource", Target = "DotNetOpenAuth.OpenId.OpenIdStrings.resources")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OAuth2.ChannelElements")] diff --git a/src/DotNetOpenAuth.Core/IEmbeddedResourceRetrieval.cs b/src/DotNetOpenAuth.Core/IEmbeddedResourceRetrieval.cs new file mode 100644 index 0000000..b9a6fd0 --- /dev/null +++ b/src/DotNetOpenAuth.Core/IEmbeddedResourceRetrieval.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// <copyright file="IEmbeddedResourceRetrieval.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + + /// <summary> + /// An interface that provides URLs from which embedded resources can be obtained. + /// </summary> + public interface IEmbeddedResourceRetrieval { + /// <summary> + /// Gets the URL from which the given manifest resource may be downloaded by the user agent. + /// </summary> + /// <param name="someTypeInResourceAssembly">Some type in the assembly containing the desired resource.</param> + /// <param name="manifestResourceName">Manifest name of the desired resource.</param> + /// <returns>An absolute URL.</returns> + Uri GetWebResourceUrl(Type someTypeInResourceAssembly, string manifestResourceName); + } +} diff --git a/src/DotNetOpenAuth.Core/Logger.cs b/src/DotNetOpenAuth.Core/Logger.cs new file mode 100644 index 0000000..c9283cd --- /dev/null +++ b/src/DotNetOpenAuth.Core/Logger.cs @@ -0,0 +1,184 @@ +//----------------------------------------------------------------------- +// <copyright file="Logger.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Diagnostics.Contracts; + using System.Globalization; + using DotNetOpenAuth.Loggers; + using DotNetOpenAuth.Messaging; + using log4net.Core; + + /// <summary> + /// A general logger for the entire DotNetOpenAuth library. + /// </summary> + /// <remarks> + /// Because this logger is intended for use with non-localized strings, the + /// overloads that take <see cref="CultureInfo"/> have been removed, and + /// <see cref="CultureInfo.InvariantCulture"/> is used implicitly. + /// </remarks> + internal static partial class Logger { + #region Category-specific loggers + + /// <summary> + /// The <see cref="ILog"/> instance that is to be used + /// by this static Logger for the duration of the appdomain. + /// </summary> + private static readonly ILog library = CreateWithBanner("DotNetOpenAuth"); + + /// <summary> + /// Backing field for the <see cref="Yadis"/> property. + /// </summary> + private static readonly ILog yadis = Create("DotNetOpenAuth.Yadis"); + + /// <summary> + /// Backing field for the <see cref="Messaging"/> property. + /// </summary> + private static readonly ILog messaging = Create("DotNetOpenAuth.Messaging"); + + /// <summary> + /// Backing field for the <see cref="Channel"/> property. + /// </summary> + private static readonly ILog channel = Create("DotNetOpenAuth.Messaging.Channel"); + + /// <summary> + /// Backing field for the <see cref="Bindings"/> property. + /// </summary> + private static readonly ILog bindings = Create("DotNetOpenAuth.Messaging.Bindings"); + + /// <summary> + /// Backing field for the <see cref="Signatures"/> property. + /// </summary> + private static readonly ILog signatures = Create("DotNetOpenAuth.Messaging.Bindings.Signatures"); + + /// <summary> + /// Backing field for the <see cref="Http"/> property. + /// </summary> + private static readonly ILog http = Create("DotNetOpenAuth.Http"); + + /// <summary> + /// Backing field for the <see cref="Controls"/> property. + /// </summary> + private static readonly ILog controls = Create("DotNetOpenAuth.Controls"); + + /// <summary> + /// Backing field for the <see cref="OpenId"/> property. + /// </summary> + private static readonly ILog openId = Create("DotNetOpenAuth.OpenId"); + + /// <summary> + /// Backing field for the <see cref="OAuth"/> property. + /// </summary> + private static readonly ILog oauth = Create("DotNetOpenAuth.OAuth"); + + /// <summary> + /// Backing field for the <see cref="InfoCard"/> property. + /// </summary> + private static readonly ILog infocard = Create("DotNetOpenAuth.InfoCard"); + + /// <summary> + /// Gets the logger for general library logging. + /// </summary> + internal static ILog Library { get { return library; } } + + /// <summary> + /// Gets the logger for service discovery and selection events. + /// </summary> + internal static ILog Yadis { get { return yadis; } } + + /// <summary> + /// Gets the logger for Messaging events. + /// </summary> + internal static ILog Messaging { get { return messaging; } } + + /// <summary> + /// Gets the logger for Channel events. + /// </summary> + internal static ILog Channel { get { return channel; } } + + /// <summary> + /// Gets the logger for binding elements and binding-element related events on the channel. + /// </summary> + internal static ILog Bindings { get { return bindings; } } + + /// <summary> + /// Gets the logger specifically used for logging verbose text on everything about the signing process. + /// </summary> + internal static ILog Signatures { get { return signatures; } } + + /// <summary> + /// Gets the logger for HTTP-level events. + /// </summary> + internal static ILog Http { get { return http; } } + + /// <summary> + /// Gets the logger for events logged by ASP.NET controls. + /// </summary> + internal static ILog Controls { get { return controls; } } + + /// <summary> + /// Gets the logger for high-level OpenID events. + /// </summary> + internal static ILog OpenId { get { return openId; } } + + /// <summary> + /// Gets the logger for high-level OAuth events. + /// </summary> + internal static ILog OAuth { get { return oauth; } } + + /// <summary> + /// Gets the logger for high-level InfoCard events. + /// </summary> + internal static ILog InfoCard { get { return infocard; } } + + #endregion + + /// <summary> + /// Creates an additional logger on demand for a subsection of the application. + /// </summary> + /// <param name="name">A name that will be included in the log file.</param> + /// <returns>The <see cref="ILog"/> instance created with the given name.</returns> + internal static ILog Create(string name) { + Requires.NotNullOrEmpty(name, "name"); + return InitializeFacade(name); + } + + /// <summary> + /// Creates the main logger for the library, and emits an INFO message + /// that is the name and version of the library. + /// </summary> + /// <param name="name">A name that will be included in the log file.</param> + /// <returns>The <see cref="ILog"/> instance created with the given name.</returns> + internal static ILog CreateWithBanner(string name) { + Requires.NotNullOrEmpty(name, "name"); + ILog log = Create(name); + log.Info(Util.LibraryVersion); + return log; + } + + /// <summary> + /// Creates an additional logger on demand for a subsection of the application. + /// </summary> + /// <param name="type">A type whose full name that will be included in the log file.</param> + /// <returns>The <see cref="ILog"/> instance created with the given type name.</returns> + internal static ILog Create(Type type) { + Requires.NotNull(type, "type"); + + return Create(type.FullName); + } + + /// <summary> + /// Discovers the presence of Log4net.dll and other logging mechanisms + /// and returns the best available logger. + /// </summary> + /// <param name="name">The name of the log to initialize.</param> + /// <returns>The <see cref="ILog"/> instance of the logger to use.</returns> + private static ILog InitializeFacade(string name) { + ILog result = Log4NetLogger.Initialize(name) ?? TraceLogger.Initialize(name) ?? NoOpLogger.Initialize(); + return result; + } + } +} diff --git a/src/DotNetOpenAuth.Core/Loggers/ILog.cs b/src/DotNetOpenAuth.Core/Loggers/ILog.cs new file mode 100644 index 0000000..8094296 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Loggers/ILog.cs @@ -0,0 +1,851 @@ +// <auto-generated /> + +#region Copyright & License +// +// Copyright 2001-2006 The Apache Software Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +// This interface is designed to look like log4net's ILog interface. +// We have this as a facade in front of it to avoid crashing if the +// hosting web site chooses not to deploy log4net.dll along with +// DotNetOpenAuth.dll. + +namespace DotNetOpenAuth.Loggers +{ + using System; + using System.Reflection; + using log4net; + using log4net.Core; + + /// <summary> + /// The ILog interface is use by application to log messages into + /// the log4net framework. + /// </summary> + /// <remarks> + /// <para> + /// Use the <see cref="LogManager"/> to obtain logger instances + /// that implement this interface. The <see cref="LogManager.GetLogger(Assembly,Type)"/> + /// static method is used to get logger instances. + /// </para> + /// <para> + /// This class contains methods for logging at different levels and also + /// has properties for determining if those logging levels are + /// enabled in the current configuration. + /// </para> + /// <para> + /// This interface can be implemented in different ways. This documentation + /// specifies reasonable behavior that a caller can expect from the actual + /// implementation, however different implementations reserve the right to + /// do things differently. + /// </para> + /// </remarks> + /// <example>Simple example of logging messages + /// <code lang="C#"> + /// ILog log = LogManager.GetLogger("application-log"); + /// + /// log.Info("Application Start"); + /// log.Debug("This is a debug message"); + /// + /// if (log.IsDebugEnabled) + /// { + /// log.Debug("This is another debug message"); + /// } + /// </code> + /// </example> + /// <seealso cref="LogManager"/> + /// <seealso cref="LogManager.GetLogger(Assembly, Type)"/> + /// <author>Nicko Cadell</author> + /// <author>Gert Driesen</author> + interface ILog + { + /// <overloads>Log a message object with the <see cref="Level.Debug"/> level.</overloads> + /// <summary> + /// Log a message object with the <see cref="Level.Debug"/> level. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <remarks> + /// <para> + /// This method first checks if this logger is <c>DEBUG</c> + /// enabled by comparing the level of this logger with the + /// <see cref="Level.Debug"/> level. If this logger is + /// <c>DEBUG</c> enabled, then it converts the message object + /// (passed as parameter) to a string by invoking the appropriate + /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then + /// proceeds to call all the registered appenders in this logger + /// and also higher in the hierarchy depending on the value of + /// the additivity flag. + /// </para> + /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/> + /// to this method will print the name of the <see cref="Exception"/> + /// but no stack trace. To print a stack trace use the + /// <see cref="Debug(object,Exception)"/> form instead. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object,Exception)"/> + /// <seealso cref="IsDebugEnabled"/> + void Debug(object message); + + /// <summary> + /// Log a message object with the <see cref="Level.Debug"/> level including + /// the stack trace of the <see cref="Exception"/> passed + /// as a parameter. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <param name="exception">The exception to log, including its stack trace.</param> + /// <remarks> + /// <para> + /// See the <see cref="Debug(object)"/> form for more detailed information. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + /// <seealso cref="IsDebugEnabled"/> + void Debug(object message, Exception exception); + + /// <overloads>Log a formatted string with the <see cref="Level.Debug"/> level.</overloads> + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Debug"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="args">An Object array containing zero or more objects to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + /// <seealso cref="IsDebugEnabled"/> + void DebugFormat(string format, params object[] args); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Debug"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + /// <seealso cref="IsDebugEnabled"/> + void DebugFormat(string format, object arg0); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Debug"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + /// <seealso cref="IsDebugEnabled"/> + void DebugFormat(string format, object arg0, object arg1); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Debug"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <param name="arg2">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + /// <seealso cref="IsDebugEnabled"/> + void DebugFormat(string format, object arg0, object arg1, object arg2); + + /// <overloads>Log a message object with the <see cref="Level.Info"/> level.</overloads> + /// <summary> + /// Logs a message object with the <see cref="Level.Info"/> level. + /// </summary> + /// <remarks> + /// <para> + /// This method first checks if this logger is <c>INFO</c> + /// enabled by comparing the level of this logger with the + /// <see cref="Level.Info"/> level. If this logger is + /// <c>INFO</c> enabled, then it converts the message object + /// (passed as parameter) to a string by invoking the appropriate + /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then + /// proceeds to call all the registered appenders in this logger + /// and also higher in the hierarchy depending on the value of the + /// additivity flag. + /// </para> + /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/> + /// to this method will print the name of the <see cref="Exception"/> + /// but no stack trace. To print a stack trace use the + /// <see cref="Info(object,Exception)"/> form instead. + /// </para> + /// </remarks> + /// <param name="message">The message object to log.</param> + /// <seealso cref="Info(object,Exception)"/> + /// <seealso cref="IsInfoEnabled"/> + void Info(object message); + + /// <summary> + /// Logs a message object with the <c>INFO</c> level including + /// the stack trace of the <see cref="Exception"/> passed + /// as a parameter. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <param name="exception">The exception to log, including its stack trace.</param> + /// <remarks> + /// <para> + /// See the <see cref="Info(object)"/> form for more detailed information. + /// </para> + /// </remarks> + /// <seealso cref="Info(object)"/> + /// <seealso cref="IsInfoEnabled"/> + void Info(object message, Exception exception); + + /// <overloads>Log a formatted message string with the <see cref="Level.Info"/> level.</overloads> + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Info"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="args">An Object array containing zero or more objects to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Info(object,Exception)"/> + /// <seealso cref="IsInfoEnabled"/> + void InfoFormat(string format, params object[] args); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Info"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Info(object)"/> + /// <seealso cref="IsInfoEnabled"/> + void InfoFormat(string format, object arg0); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Info"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Info(object)"/> + /// <seealso cref="IsInfoEnabled"/> + void InfoFormat(string format, object arg0, object arg1); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Info"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <param name="arg2">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Info(object)"/> + /// <seealso cref="IsInfoEnabled"/> + void InfoFormat(string format, object arg0, object arg1, object arg2); + + /// <overloads>Log a message object with the <see cref="Level.Warn"/> level.</overloads> + /// <summary> + /// Log a message object with the <see cref="Level.Warn"/> level. + /// </summary> + /// <remarks> + /// <para> + /// This method first checks if this logger is <c>WARN</c> + /// enabled by comparing the level of this logger with the + /// <see cref="Level.Warn"/> level. If this logger is + /// <c>WARN</c> enabled, then it converts the message object + /// (passed as parameter) to a string by invoking the appropriate + /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then + /// proceeds to call all the registered appenders in this logger + /// and also higher in the hierarchy depending on the value of the + /// additivity flag. + /// </para> + /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/> + /// to this method will print the name of the <see cref="Exception"/> + /// but no stack trace. To print a stack trace use the + /// <see cref="Warn(object,Exception)"/> form instead. + /// </para> + /// </remarks> + /// <param name="message">The message object to log.</param> + /// <seealso cref="Warn(object,Exception)"/> + /// <seealso cref="IsWarnEnabled"/> + void Warn(object message); + + /// <summary> + /// Log a message object with the <see cref="Level.Warn"/> level including + /// the stack trace of the <see cref="Exception"/> passed + /// as a parameter. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <param name="exception">The exception to log, including its stack trace.</param> + /// <remarks> + /// <para> + /// See the <see cref="Warn(object)"/> form for more detailed information. + /// </para> + /// </remarks> + /// <seealso cref="Warn(object)"/> + /// <seealso cref="IsWarnEnabled"/> + void Warn(object message, Exception exception); + + /// <overloads>Log a formatted message string with the <see cref="Level.Warn"/> level.</overloads> + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Warn"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="args">An Object array containing zero or more objects to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Warn(object,Exception)"/> + /// <seealso cref="IsWarnEnabled"/> + void WarnFormat(string format, params object[] args); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Warn"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Warn(object)"/> + /// <seealso cref="IsWarnEnabled"/> + void WarnFormat(string format, object arg0); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Warn"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Warn(object)"/> + /// <seealso cref="IsWarnEnabled"/> + void WarnFormat(string format, object arg0, object arg1); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Warn"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <param name="arg2">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Warn(object)"/> + /// <seealso cref="IsWarnEnabled"/> + void WarnFormat(string format, object arg0, object arg1, object arg2); + + /// <overloads>Log a message object with the <see cref="Level.Error"/> level.</overloads> + /// <summary> + /// Logs a message object with the <see cref="Level.Error"/> level. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <remarks> + /// <para> + /// This method first checks if this logger is <c>ERROR</c> + /// enabled by comparing the level of this logger with the + /// <see cref="Level.Error"/> level. If this logger is + /// <c>ERROR</c> enabled, then it converts the message object + /// (passed as parameter) to a string by invoking the appropriate + /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then + /// proceeds to call all the registered appenders in this logger + /// and also higher in the hierarchy depending on the value of the + /// additivity flag. + /// </para> + /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/> + /// to this method will print the name of the <see cref="Exception"/> + /// but no stack trace. To print a stack trace use the + /// <see cref="Error(object,Exception)"/> form instead. + /// </para> + /// </remarks> + /// <seealso cref="Error(object,Exception)"/> + /// <seealso cref="IsErrorEnabled"/> + void Error(object message); + + /// <summary> + /// Log a message object with the <see cref="Level.Error"/> level including + /// the stack trace of the <see cref="Exception"/> passed + /// as a parameter. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <param name="exception">The exception to log, including its stack trace.</param> + /// <remarks> + /// <para> + /// See the <see cref="Error(object)"/> form for more detailed information. + /// </para> + /// </remarks> + /// <seealso cref="Error(object)"/> + /// <seealso cref="IsErrorEnabled"/> + void Error(object message, Exception exception); + + /// <overloads>Log a formatted message string with the <see cref="Level.Error"/> level.</overloads> + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Error"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="args">An Object array containing zero or more objects to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Error(object,Exception)"/> + /// <seealso cref="IsErrorEnabled"/> + void ErrorFormat(string format, params object[] args); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Error"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Error(object)"/> + /// <seealso cref="IsErrorEnabled"/> + void ErrorFormat(string format, object arg0); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Error"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Error(object)"/> + /// <seealso cref="IsErrorEnabled"/> + void ErrorFormat(string format, object arg0, object arg1); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Error"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <param name="arg2">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Error(object)"/> + /// <seealso cref="IsErrorEnabled"/> + void ErrorFormat(string format, object arg0, object arg1, object arg2); + + /// <overloads>Log a message object with the <see cref="Level.Fatal"/> level.</overloads> + /// <summary> + /// Log a message object with the <see cref="Level.Fatal"/> level. + /// </summary> + /// <remarks> + /// <para> + /// This method first checks if this logger is <c>FATAL</c> + /// enabled by comparing the level of this logger with the + /// <see cref="Level.Fatal"/> level. If this logger is + /// <c>FATAL</c> enabled, then it converts the message object + /// (passed as parameter) to a string by invoking the appropriate + /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then + /// proceeds to call all the registered appenders in this logger + /// and also higher in the hierarchy depending on the value of the + /// additivity flag. + /// </para> + /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/> + /// to this method will print the name of the <see cref="Exception"/> + /// but no stack trace. To print a stack trace use the + /// <see cref="Fatal(object,Exception)"/> form instead. + /// </para> + /// </remarks> + /// <param name="message">The message object to log.</param> + /// <seealso cref="Fatal(object,Exception)"/> + /// <seealso cref="IsFatalEnabled"/> + void Fatal(object message); + + /// <summary> + /// Log a message object with the <see cref="Level.Fatal"/> level including + /// the stack trace of the <see cref="Exception"/> passed + /// as a parameter. + /// </summary> + /// <param name="message">The message object to log.</param> + /// <param name="exception">The exception to log, including its stack trace.</param> + /// <remarks> + /// <para> + /// See the <see cref="Fatal(object)"/> form for more detailed information. + /// </para> + /// </remarks> + /// <seealso cref="Fatal(object)"/> + /// <seealso cref="IsFatalEnabled"/> + void Fatal(object message, Exception exception); + + /// <overloads>Log a formatted message string with the <see cref="Level.Fatal"/> level.</overloads> + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Fatal"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="args">An Object array containing zero or more objects to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Fatal(object,Exception)"/> + /// <seealso cref="IsFatalEnabled"/> + void FatalFormat(string format, params object[] args); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Fatal"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Fatal(object)"/> + /// <seealso cref="IsFatalEnabled"/> + void FatalFormat(string format, object arg0); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Fatal"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Fatal(object)"/> + /// <seealso cref="IsFatalEnabled"/> + void FatalFormat(string format, object arg0, object arg1); + + /// <summary> + /// Logs a formatted message string with the <see cref="Level.Fatal"/> level. + /// </summary> + /// <param name="format">A String containing zero or more format items</param> + /// <param name="arg0">An Object to format</param> + /// <param name="arg1">An Object to format</param> + /// <param name="arg2">An Object to format</param> + /// <remarks> + /// <para> + /// The message is formatted using the <c>String.Format</c> method. See + /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior + /// of the formatting. + /// </para> + /// <para> + /// This method does not take an <see cref="Exception"/> object to include in the + /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/> + /// methods instead. + /// </para> + /// </remarks> + /// <seealso cref="Fatal(object)"/> + /// <seealso cref="IsFatalEnabled"/> + void FatalFormat(string format, object arg0, object arg1, object arg2); + + /// <summary> + /// Checks if this logger is enabled for the <see cref="Level.Debug"/> level. + /// </summary> + /// <value> + /// <c>true</c> if this logger is enabled for <see cref="Level.Debug"/> events, <c>false</c> otherwise. + /// </value> + /// <remarks> + /// <para> + /// This function is intended to lessen the computational cost of + /// disabled log debug statements. + /// </para> + /// <para> For some ILog interface <c>log</c>, when you write:</para> + /// <code lang="C#"> + /// log.Debug("This is entry number: " + i ); + /// </code> + /// <para> + /// You incur the cost constructing the message, string construction and concatenation in + /// this case, regardless of whether the message is logged or not. + /// </para> + /// <para> + /// If you are worried about speed (who isn't), then you should write: + /// </para> + /// <code lang="C#"> + /// if (log.IsDebugEnabled) + /// { + /// log.Debug("This is entry number: " + i ); + /// } + /// </code> + /// <para> + /// This way you will not incur the cost of parameter + /// construction if debugging is disabled for <c>log</c>. On + /// the other hand, if the <c>log</c> is debug enabled, you + /// will incur the cost of evaluating whether the logger is debug + /// enabled twice. Once in <see cref="IsDebugEnabled"/> and once in + /// the <see cref="Debug(object)"/>. This is an insignificant overhead + /// since evaluating a logger takes about 1% of the time it + /// takes to actually log. This is the preferred style of logging. + /// </para> + /// <para>Alternatively if your logger is available statically then the is debug + /// enabled state can be stored in a static variable like this: + /// </para> + /// <code lang="C#"> + /// private static readonly bool isDebugEnabled = log.IsDebugEnabled; + /// </code> + /// <para> + /// Then when you come to log you can write: + /// </para> + /// <code lang="C#"> + /// if (isDebugEnabled) + /// { + /// log.Debug("This is entry number: " + i ); + /// } + /// </code> + /// <para> + /// This way the debug enabled state is only queried once + /// when the class is loaded. Using a <c>private static readonly</c> + /// variable is the most efficient because it is a run time constant + /// and can be heavily optimized by the JIT compiler. + /// </para> + /// <para> + /// Of course if you use a static readonly variable to + /// hold the enabled state of the logger then you cannot + /// change the enabled state at runtime to vary the logging + /// that is produced. You have to decide if you need absolute + /// speed or runtime flexibility. + /// </para> + /// </remarks> + /// <seealso cref="Debug(object)"/> + bool IsDebugEnabled { get; } + + /// <summary> + /// Checks if this logger is enabled for the <see cref="Level.Info"/> level. + /// </summary> + /// <value> + /// <c>true</c> if this logger is enabled for <see cref="Level.Info"/> events, <c>false</c> otherwise. + /// </value> + /// <remarks> + /// For more information see <see cref="ILog.IsDebugEnabled"/>. + /// </remarks> + /// <seealso cref="Info(object)"/> + /// <seealso cref="ILog.IsDebugEnabled"/> + bool IsInfoEnabled { get; } + + /// <summary> + /// Checks if this logger is enabled for the <see cref="Level.Warn"/> level. + /// </summary> + /// <value> + /// <c>true</c> if this logger is enabled for <see cref="Level.Warn"/> events, <c>false</c> otherwise. + /// </value> + /// <remarks> + /// For more information see <see cref="ILog.IsDebugEnabled"/>. + /// </remarks> + /// <seealso cref="Warn(object)"/> + /// <seealso cref="ILog.IsDebugEnabled"/> + bool IsWarnEnabled { get; } + + /// <summary> + /// Checks if this logger is enabled for the <see cref="Level.Error"/> level. + /// </summary> + /// <value> + /// <c>true</c> if this logger is enabled for <see cref="Level.Error"/> events, <c>false</c> otherwise. + /// </value> + /// <remarks> + /// For more information see <see cref="ILog.IsDebugEnabled"/>. + /// </remarks> + /// <seealso cref="Error(object)"/> + /// <seealso cref="ILog.IsDebugEnabled"/> + bool IsErrorEnabled { get; } + + /// <summary> + /// Checks if this logger is enabled for the <see cref="Level.Fatal"/> level. + /// </summary> + /// <value> + /// <c>true</c> if this logger is enabled for <see cref="Level.Fatal"/> events, <c>false</c> otherwise. + /// </value> + /// <remarks> + /// For more information see <see cref="ILog.IsDebugEnabled"/>. + /// </remarks> + /// <seealso cref="Fatal(object)"/> + /// <seealso cref="ILog.IsDebugEnabled"/> + bool IsFatalEnabled { get; } + } +} diff --git a/src/DotNetOpenAuth.Core/Loggers/Log4NetLogger.cs b/src/DotNetOpenAuth.Core/Loggers/Log4NetLogger.cs new file mode 100644 index 0000000..dd71a05 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Loggers/Log4NetLogger.cs @@ -0,0 +1,215 @@ +// <auto-generated /> + +namespace DotNetOpenAuth.Loggers { + using System; + using System.Globalization; + using System.IO; + using System.Reflection; + + internal class Log4NetLogger : ILog { + private log4net.ILog log4netLogger; + + private Log4NetLogger(log4net.ILog logger) { + this.log4netLogger = logger; + } + + #region ILog Members + + public bool IsDebugEnabled { + get { return this.log4netLogger.IsDebugEnabled; } + } + + public bool IsInfoEnabled { + get { return this.log4netLogger.IsInfoEnabled; } + } + + public bool IsWarnEnabled { + get { return this.log4netLogger.IsWarnEnabled; } + } + + public bool IsErrorEnabled { + get { return this.log4netLogger.IsErrorEnabled; } + } + + public bool IsFatalEnabled { + get { return this.log4netLogger.IsFatalEnabled; } + } + + #endregion + + private static bool IsLog4NetPresent { + get { + try { + Assembly.Load("log4net"); + return true; + } catch (FileNotFoundException) { + return false; + } + } + } + + #region ILog methods + + public void Debug(object message) { + this.log4netLogger.Debug(message); + } + + public void Debug(object message, Exception exception) { + this.log4netLogger.Debug(message, exception); + } + + public void DebugFormat(string format, params object[] args) { + this.log4netLogger.DebugFormat(CultureInfo.InvariantCulture, format, args); + } + + public void DebugFormat(string format, object arg0) { + this.log4netLogger.DebugFormat(format, arg0); + } + + public void DebugFormat(string format, object arg0, object arg1) { + this.log4netLogger.DebugFormat(format, arg0, arg1); + } + + public void DebugFormat(string format, object arg0, object arg1, object arg2) { + this.log4netLogger.DebugFormat(format, arg0, arg1, arg2); + } + + public void DebugFormat(IFormatProvider provider, string format, params object[] args) { + this.log4netLogger.DebugFormat(provider, format, args); + } + + public void Info(object message) { + this.log4netLogger.Info(message); + } + + public void Info(object message, Exception exception) { + this.log4netLogger.Info(message, exception); + } + + public void InfoFormat(string format, params object[] args) { + this.log4netLogger.InfoFormat(CultureInfo.InvariantCulture, format, args); + } + + public void InfoFormat(string format, object arg0) { + this.log4netLogger.InfoFormat(format, arg0); + } + + public void InfoFormat(string format, object arg0, object arg1) { + this.log4netLogger.InfoFormat(format, arg0, arg1); + } + + public void InfoFormat(string format, object arg0, object arg1, object arg2) { + this.log4netLogger.InfoFormat(format, arg0, arg1, arg2); + } + + public void InfoFormat(IFormatProvider provider, string format, params object[] args) { + this.log4netLogger.InfoFormat(provider, format, args); + } + + public void Warn(object message) { + this.log4netLogger.Warn(message); + } + + public void Warn(object message, Exception exception) { + this.log4netLogger.Warn(message, exception); + } + + public void WarnFormat(string format, params object[] args) { + this.log4netLogger.WarnFormat(CultureInfo.InvariantCulture, format, args); + } + + public void WarnFormat(string format, object arg0) { + this.log4netLogger.WarnFormat(format, arg0); + } + + public void WarnFormat(string format, object arg0, object arg1) { + this.log4netLogger.WarnFormat(format, arg0, arg1); + } + + public void WarnFormat(string format, object arg0, object arg1, object arg2) { + this.log4netLogger.WarnFormat(format, arg0, arg1, arg2); + } + + public void WarnFormat(IFormatProvider provider, string format, params object[] args) { + this.log4netLogger.WarnFormat(provider, format, args); + } + + public void Error(object message) { + this.log4netLogger.Error(message); + } + + public void Error(object message, Exception exception) { + this.log4netLogger.Error(message, exception); + } + + public void ErrorFormat(string format, params object[] args) { + this.log4netLogger.ErrorFormat(CultureInfo.InvariantCulture, format, args); + } + + public void ErrorFormat(string format, object arg0) { + this.log4netLogger.ErrorFormat(format, arg0); + } + + public void ErrorFormat(string format, object arg0, object arg1) { + this.log4netLogger.ErrorFormat(format, arg0, arg1); + } + + public void ErrorFormat(string format, object arg0, object arg1, object arg2) { + this.log4netLogger.ErrorFormat(format, arg0, arg1, arg2); + } + + public void ErrorFormat(IFormatProvider provider, string format, params object[] args) { + this.log4netLogger.ErrorFormat(provider, format, args); + } + + public void Fatal(object message) { + this.log4netLogger.Fatal(message); + } + + public void Fatal(object message, Exception exception) { + this.log4netLogger.Fatal(message, exception); + } + + public void FatalFormat(string format, params object[] args) { + this.log4netLogger.FatalFormat(CultureInfo.InvariantCulture, format, args); + } + + public void FatalFormat(string format, object arg0) { + this.log4netLogger.FatalFormat(format, arg0); + } + + public void FatalFormat(string format, object arg0, object arg1) { + this.log4netLogger.FatalFormat(format, arg0, arg1); + } + + public void FatalFormat(string format, object arg0, object arg1, object arg2) { + this.log4netLogger.FatalFormat(format, arg0, arg1, arg2); + } + + public void FatalFormat(IFormatProvider provider, string format, params object[] args) { + this.log4netLogger.FatalFormat(provider, format, args); + } + + #endregion + + /// <summary> + /// Returns a new log4net logger if it exists, or returns null if the assembly cannot be found. + /// </summary> + /// <returns>The created <see cref="ILog"/> instance.</returns> + internal static ILog Initialize(string name) { + try { + return IsLog4NetPresent ? CreateLogger(name) : null; + } catch (FileLoadException) { // wrong log4net.dll version + return null; + } + } + + /// <summary> + /// Creates the log4net.LogManager. Call ONLY after log4net.dll is known to be present. + /// </summary> + /// <returns>The created <see cref="ILog"/> instance.</returns> + private static ILog CreateLogger(string name) { + return new Log4NetLogger(log4net.LogManager.GetLogger(name)); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Loggers/NoOpLogger.cs b/src/DotNetOpenAuth.Core/Loggers/NoOpLogger.cs new file mode 100644 index 0000000..7d1b37f --- /dev/null +++ b/src/DotNetOpenAuth.Core/Loggers/NoOpLogger.cs @@ -0,0 +1,159 @@ +// <auto-generated /> + +namespace DotNetOpenAuth.Loggers { + using System; + + internal class NoOpLogger : ILog { + #region ILog Members + + public bool IsDebugEnabled { + get { return false; } + } + + public bool IsInfoEnabled { + get { return false; } + } + + public bool IsWarnEnabled { + get { return false; } + } + + public bool IsErrorEnabled { + get { return false; } + } + + public bool IsFatalEnabled { + get { return false; } + } + + public void Debug(object message) { + return; + } + + public void Debug(object message, Exception exception) { + return; + } + + public void DebugFormat(string format, params object[] args) { + return; + } + + public void DebugFormat(string format, object arg0) { + return; + } + + public void DebugFormat(string format, object arg0, object arg1) { + return; + } + + public void DebugFormat(string format, object arg0, object arg1, object arg2) { + return; + } + + public void Info(object message) { + return; + } + + public void Info(object message, Exception exception) { + return; + } + + public void InfoFormat(string format, params object[] args) { + return; + } + + public void InfoFormat(string format, object arg0) { + return; + } + + public void InfoFormat(string format, object arg0, object arg1) { + return; + } + + public void InfoFormat(string format, object arg0, object arg1, object arg2) { + return; + } + + public void Warn(object message) { + return; + } + + public void Warn(object message, Exception exception) { + return; + } + + public void WarnFormat(string format, params object[] args) { + return; + } + + public void WarnFormat(string format, object arg0) { + return; + } + + public void WarnFormat(string format, object arg0, object arg1) { + return; + } + + public void WarnFormat(string format, object arg0, object arg1, object arg2) { + return; + } + + public void Error(object message) { + return; + } + + public void Error(object message, Exception exception) { + return; + } + + public void ErrorFormat(string format, params object[] args) { + return; + } + + public void ErrorFormat(string format, object arg0) { + return; + } + + public void ErrorFormat(string format, object arg0, object arg1) { + return; + } + + public void ErrorFormat(string format, object arg0, object arg1, object arg2) { + return; + } + + public void Fatal(object message) { + return; + } + + public void Fatal(object message, Exception exception) { + return; + } + + public void FatalFormat(string format, params object[] args) { + return; + } + + public void FatalFormat(string format, object arg0) { + return; + } + + public void FatalFormat(string format, object arg0, object arg1) { + return; + } + + public void FatalFormat(string format, object arg0, object arg1, object arg2) { + return; + } + + #endregion + + /// <summary> + /// Returns a new logger that does nothing when invoked. + /// </summary> + /// <returns>The created <see cref="ILog"/> instance.</returns> + internal static ILog Initialize() { + return new NoOpLogger(); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Loggers/TraceLogger.cs b/src/DotNetOpenAuth.Core/Loggers/TraceLogger.cs new file mode 100644 index 0000000..9b0bb0f --- /dev/null +++ b/src/DotNetOpenAuth.Core/Loggers/TraceLogger.cs @@ -0,0 +1,297 @@ +// <auto-generated /> + +namespace DotNetOpenAuth.Loggers { + using System; + using System.Diagnostics; + using System.Security; + using System.Security.Permissions; + + internal class TraceLogger : ILog { + private TraceSwitch traceSwitch; + + internal TraceLogger(string name) { + traceSwitch = new TraceSwitch(name, name + " Trace Switch"); + } + + #region ILog Properties + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public bool IsDebugEnabled { + get { return this.traceSwitch.TraceVerbose; } + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public bool IsInfoEnabled { + get { return this.traceSwitch.TraceInfo; } + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public bool IsWarnEnabled { + get { return this.traceSwitch.TraceWarning; } + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public bool IsErrorEnabled { + get { return this.traceSwitch.TraceError; } + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public bool IsFatalEnabled { + get { return this.traceSwitch.TraceError; } + } + + #endregion + + private static bool IsSufficientPermissionGranted { + get { + PermissionSet permissions = new PermissionSet(PermissionState.None); + permissions.AddPermission(new KeyContainerPermission(PermissionState.Unrestricted)); + permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); + permissions.AddPermission(new RegistryPermission(PermissionState.Unrestricted)); + permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlEvidence | SecurityPermissionFlag.UnmanagedCode | SecurityPermissionFlag.ControlThread)); + var file = new FileIOPermission(PermissionState.None); + file.AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read; + permissions.AddPermission(file); + try { + permissions.Demand(); + return true; + } catch (SecurityException) { + return false; + } + } + } + + #region ILog Methods + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Debug(object message) { + Trace.TraceInformation(message.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Debug(object message, Exception exception) { + Trace.TraceInformation(message + ": " + exception.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void DebugFormat(string format, params object[] args) { + Trace.TraceInformation(format, args); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void DebugFormat(string format, object arg0) { + Trace.TraceInformation(format, arg0); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void DebugFormat(string format, object arg0, object arg1) { + Trace.TraceInformation(format, arg0, arg1); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void DebugFormat(string format, object arg0, object arg1, object arg2) { + Trace.TraceInformation(format, arg0, arg1, arg2); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Info(object message) { + Trace.TraceInformation(message.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Info(object message, Exception exception) { + Trace.TraceInformation(message + ": " + exception.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void InfoFormat(string format, params object[] args) { + Trace.TraceInformation(format, args); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void InfoFormat(string format, object arg0) { + Trace.TraceInformation(format, arg0); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void InfoFormat(string format, object arg0, object arg1) { + Trace.TraceInformation(format, arg0, arg1); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void InfoFormat(string format, object arg0, object arg1, object arg2) { + Trace.TraceInformation(format, arg0, arg1, arg2); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Warn(object message) { + Trace.TraceWarning(message.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Warn(object message, Exception exception) { + Trace.TraceWarning(message + ": " + exception.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void WarnFormat(string format, params object[] args) { + Trace.TraceWarning(format, args); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void WarnFormat(string format, object arg0) { + Trace.TraceWarning(format, arg0); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void WarnFormat(string format, object arg0, object arg1) { + Trace.TraceWarning(format, arg0, arg1); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void WarnFormat(string format, object arg0, object arg1, object arg2) { + Trace.TraceWarning(format, arg0, arg1, arg2); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Error(object message) { + Trace.TraceError(message.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Error(object message, Exception exception) { + Trace.TraceError(message + ": " + exception.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void ErrorFormat(string format, params object[] args) { + Trace.TraceError(format, args); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void ErrorFormat(string format, object arg0) { + Trace.TraceError(format, arg0); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void ErrorFormat(string format, object arg0, object arg1) { + Trace.TraceError(format, arg0, arg1); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void ErrorFormat(string format, object arg0, object arg1, object arg2) { + Trace.TraceError(format, arg0, arg1, arg2); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Fatal(object message) { + Trace.TraceError(message.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void Fatal(object message, Exception exception) { + Trace.TraceError(message + ": " + exception.ToString()); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void FatalFormat(string format, params object[] args) { + Trace.TraceError(format, args); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void FatalFormat(string format, object arg0) { + Trace.TraceError(format, arg0); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void FatalFormat(string format, object arg0, object arg1) { + Trace.TraceError(format, arg0, arg1); + } + + /// <summary> + /// See <see cref="ILog"/>. + /// </summary> + public void FatalFormat(string format, object arg0, object arg1, object arg2) { + Trace.TraceError(format, arg0, arg1, arg2); + } + + #endregion + + /// <summary> + /// Returns a new logger that uses the <see cref="System.Diagnostics.Trace"/> class + /// if sufficient CAS permissions are granted to use it, otherwise returns false. + /// </summary> + /// <returns>The created <see cref="ILog"/> instance.</returns> + internal static ILog Initialize(string name) { + return IsSufficientPermissionGranted ? new TraceLogger(name) : null; + } + } +} 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..129a03d --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs @@ -0,0 +1,385 @@ +//----------------------------------------------------------------------- +// <copyright file="ErrorUtilities.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.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); + } + + /// <summary> + /// Obtains a value from the dictionary if possible, or throws a <see cref="ProtocolException"/> if it's missing. + /// </summary> + /// <typeparam name="TKey">The type of key in the dictionary.</typeparam> + /// <typeparam name="TValue">The type of value in the dictionary.</typeparam> + /// <param name="dictionary">The dictionary.</param> + /// <param name="key">The key to use to look up the value.</param> + /// <param name="message">The message to claim is invalid if the key cannot be found.</param> + /// <returns>The value for the given key.</returns> + [Pure] + internal static TValue GetValueOrThrow<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, IMessage message) { + Requires.NotNull(dictionary, "dictionary"); + Requires.NotNull(message, "message"); + + TValue value; + VerifyProtocol(dictionary.TryGetValue(key, out value), MessagingStrings.ExpectedParameterWasMissing, key, message.GetType().Name); + return value; + } + } +} 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..3ad2bdd --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs @@ -0,0 +1,684 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.239 +// +// 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 part {0} was expected in the {1} message but was not found.. + /// </summary> + internal static string ExpectedParameterWasMissing { + get { + return ResourceManager.GetString("ExpectedParameterWasMissing", 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..5f3f79a --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx @@ -0,0 +1,327 @@ +<?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> + <data name="ExpectedParameterWasMissing" xml:space="preserve"> + <value>The message part {0} was expected in the {1} message but was not found.</value> + </data> +</root>
\ No newline at end of file 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..2a94791 --- /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, HttpResponseBase 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..026b7c2 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs @@ -0,0 +1,344 @@ +//----------------------------------------------------------------------- +// <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(new HttpContextWrapper(context), true); + } + + /// <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(HttpContextBase 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 void Respond(HttpContext context) { + Requires.NotNull(context, "context"); + this.Respond(new HttpContextWrapper(context)); + } + + /// <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(HttpContextBase 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 void Respond(HttpContext context, bool endRequest) { + this.Respond(new HttpContextWrapper(context), endRequest); + } + + /// <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(HttpContextBase 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); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Migrated rules for DotNetOpenAuth.ruleset b/src/DotNetOpenAuth.Core/Migrated rules for DotNetOpenAuth.ruleset new file mode 100644 index 0000000..0ba4e6e --- /dev/null +++ b/src/DotNetOpenAuth.Core/Migrated rules for DotNetOpenAuth.ruleset @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<RuleSet Name="Migrated rules for DotNetOpenAuth.ruleset" Description="This rule set was created from the CodeAnalysisRules property for the "Debug (Any CPU)" configuration in project "C:\Users\andarno\git\dotnetopenid\src\DotNetOpenAuth\DotNetOpenAuth.csproj"." ToolsVersion="10.0"> + <IncludeAll Action="Warning" /> + <Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed"> + <Rule Id="CA1054" Action="None" /> + <Rule Id="CA1055" Action="None" /> + <Rule Id="CA1056" Action="None" /> + <Rule Id="CA1062" Action="None" /> + <Rule Id="CA2104" Action="None" /> + </Rules> +</RuleSet>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..205d331 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------- +// <copyright file="AssemblyInfo.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +// We DON'T put an AssemblyVersionAttribute in here because it is generated in the build. + +using System; +using System.Diagnostics.Contracts; +using System.Net; +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; +using System.Web.UI; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DotNetOpenAuth")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DotNetOpenAuth")] +[assembly: AssemblyCopyright("Copyright © 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en-US")] +[assembly: CLSCompliant(true)] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7d73990c-47c0-4256-9f20-a893add9e289")] + +[assembly: ContractVerification(true)] + +#if StrongNameSigned +// See comment at top of this file. We need this so that strong-naming doesn't +// keep this assembly from being useful to shared host (medium trust) web sites. +[assembly: AllowPartiallyTrustedCallers] + +[assembly: InternalsVisibleTo("DotNetOpenAuth.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.Core.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.InfoCard, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.InfoCard.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.RelyingParty, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.RelyingParty.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.Provider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.Provider.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenIdInfoCard.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.Consumer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.ServiceProvider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.AuthorizationServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.ResourceServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +#else +[assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.InfoCard")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.InfoCard.UI")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.UI")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.RelyingParty")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.RelyingParty.UI")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.Provider")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenId.Provider.UI")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OpenIdInfoCard.UI")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.Consumer")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.ServiceProvider")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.AuthorizationServer")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.ResourceServer")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif diff --git a/src/DotNetOpenAuth.Core/Reporting.cs b/src/DotNetOpenAuth.Core/Reporting.cs new file mode 100644 index 0000000..0bbbcec --- /dev/null +++ b/src/DotNetOpenAuth.Core/Reporting.cs @@ -0,0 +1,900 @@ +//----------------------------------------------------------------------- +// <copyright file="Reporting.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + 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.IO.IsolatedStorage; + using System.Linq; + using System.Net; + using System.Reflection; + using System.Security; + using System.Text; + using System.Threading; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// The statistical reporting mechanism used so this library's project authors + /// know what versions and features are in use. + /// </summary> + public class Reporting { + /// <summary> + /// A UTF8 encoder that doesn't emit the preamble. Used for mid-stream writers. + /// </summary> + private static readonly Encoding Utf8NoPreamble = new UTF8Encoding(false); + + /// <summary> + /// A value indicating whether reporting is desirable or not. Must be logical-AND'd with !<see cref="broken"/>. + /// </summary> + private static bool enabled; + + /// <summary> + /// A value indicating whether reporting experienced an error and cannot be enabled. + /// </summary> + private static bool broken; + + /// <summary> + /// A value indicating whether the reporting class has been initialized or not. + /// </summary> + private static bool initialized; + + /// <summary> + /// The object to lock during initialization. + /// </summary> + private static object initializationSync = new object(); + + /// <summary> + /// The isolated storage to use for collecting data in between published reports. + /// </summary> + private static IsolatedStorageFile file; + + /// <summary> + /// The GUID that shows up at the top of all reports from this user/machine/domain. + /// </summary> + private static Guid reportOriginIdentity; + + /// <summary> + /// The recipient of collected reports. + /// </summary> + private static Uri wellKnownPostLocation = new Uri("https://reports.dotnetopenauth.net/ReportingPost.ashx"); + + /// <summary> + /// The outgoing HTTP request handler to use for publishing reports. + /// </summary> + private static IDirectWebRequestHandler webRequestHandler; + + /// <summary> + /// A few HTTP request hosts and paths we've seen. + /// </summary> + private static PersistentHashSet observedRequests; + + /// <summary> + /// Cultures that have come in via HTTP requests. + /// </summary> + private static PersistentHashSet observedCultures; + + /// <summary> + /// Features that have been used. + /// </summary> + private static PersistentHashSet observedFeatures; + + /// <summary> + /// A collection of all the observations to include in the report. + /// </summary> + private static List<PersistentHashSet> observations = new List<PersistentHashSet>(); + + /// <summary> + /// The named events that we have counters for. + /// </summary> + private static Dictionary<string, PersistentCounter> events = new Dictionary<string, PersistentCounter>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// The lock acquired while considering whether to publish a report. + /// </summary> + private static object publishingConsiderationLock = new object(); + + /// <summary> + /// The time that we last published reports. + /// </summary> + private static DateTime lastPublished = DateTime.Now; + + /// <summary> + /// Initializes static members of the <see cref="Reporting"/> class. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "We do more than field initialization here.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Reporting MUST NOT cause unhandled exceptions.")] + static Reporting() { + Enabled = DotNetOpenAuthSection.Reporting.Enabled; + } + + /// <summary> + /// Gets or sets a value indicating whether this reporting is enabled. + /// </summary> + /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> + /// <remarks> + /// Setting this property to <c>true</c> <i>may</i> have no effect + /// if reporting has already experienced a failure of some kind. + /// </remarks> + public static bool Enabled { + get { + return enabled && !broken; + } + + set { + if (value) { + Initialize(); + } + + // Only set the static field here, so that other threads + // don't try to use reporting while we're initializing it. + enabled = value; + } + } + + /// <summary> + /// Gets the observed features. + /// </summary> + internal static PersistentHashSet ObservedFeatures { + get { return observedFeatures; } + } + + /// <summary> + /// Gets the configuration to use for reporting. + /// </summary> + internal static ReportingElement Configuration { + get { return DotNetOpenAuthSection.Reporting; } + } + + /// <summary> + /// Records an event occurrence. + /// </summary> + /// <param name="eventName">Name of the event.</param> + /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "PersistentCounter instances are stored in a table for later use.")] + internal static void RecordEventOccurrence(string eventName, string category) { + Contract.Requires(!String.IsNullOrEmpty(eventName)); + + // In release builds, just quietly return. + if (string.IsNullOrEmpty(eventName)) { + return; + } + + if (Enabled && Configuration.IncludeEventStatistics) { + PersistentCounter counter; + lock (events) { + if (!events.TryGetValue(eventName, out counter)) { + events[eventName] = counter = new PersistentCounter(file, "event-" + SanitizeFileName(eventName) + ".txt"); + } + } + + counter.Increment(category); + Touch(); + } + } + + /// <summary> + /// Records an event occurence. + /// </summary> + /// <param name="eventNameByObjectType">The object whose type name is the event name to record.</param> + /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + internal static void RecordEventOccurrence(object eventNameByObjectType, string category) { + Contract.Requires(eventNameByObjectType != null); + + // In release builds, just quietly return. + if (eventNameByObjectType == null) { + return; + } + + if (Enabled && Configuration.IncludeEventStatistics) { + RecordEventOccurrence(eventNameByObjectType.GetType().Name, category); + } + } + + /// <summary> + /// Records the use of a feature by name. + /// </summary> + /// <param name="feature">The feature.</param> + internal static void RecordFeatureUse(string feature) { + Contract.Requires(!String.IsNullOrEmpty(feature)); + + // In release builds, just quietly return. + if (string.IsNullOrEmpty(feature)) { + return; + } + + if (Enabled && Configuration.IncludeFeatureUsage) { + observedFeatures.Add(feature); + Touch(); + } + } + + /// <summary> + /// Records the use of a feature by object type. + /// </summary> + /// <param name="value">The object whose type is the feature to set as used.</param> + internal static void RecordFeatureUse(object value) { + Contract.Requires(value != null); + + // In release builds, just quietly return. + if (value == null) { + return; + } + + if (Enabled && Configuration.IncludeFeatureUsage) { + observedFeatures.Add(value.GetType().Name); + Touch(); + } + } + + /// <summary> + /// Records the use of a feature by object type. + /// </summary> + /// <param name="value">The object whose type is the feature to set as used.</param> + /// <param name="dependency1">Some dependency used by <paramref name="value"/>.</param> + internal static void RecordFeatureAndDependencyUse(object value, object dependency1) { + Contract.Requires(value != null); + + // In release builds, just quietly return. + if (value == null) { + return; + } + + if (Enabled && Configuration.IncludeFeatureUsage) { + StringBuilder builder = new StringBuilder(); + builder.Append(value.GetType().Name); + builder.Append(" "); + builder.Append(dependency1 != null ? dependency1.GetType().Name : "(null)"); + observedFeatures.Add(builder.ToString()); + Touch(); + } + } + + /// <summary> + /// Records the use of a feature by object type. + /// </summary> + /// <param name="value">The object whose type is the feature to set as used.</param> + /// <param name="dependency1">Some dependency used by <paramref name="value"/>.</param> + /// <param name="dependency2">Some dependency used by <paramref name="value"/>.</param> + internal static void RecordFeatureAndDependencyUse(object value, object dependency1, object dependency2) { + Contract.Requires(value != null); + + // In release builds, just quietly return. + if (value == null) { + return; + } + + if (Enabled && Configuration.IncludeFeatureUsage) { + StringBuilder builder = new StringBuilder(); + builder.Append(value.GetType().Name); + builder.Append(" "); + builder.Append(dependency1 != null ? dependency1.GetType().Name : "(null)"); + builder.Append(" "); + builder.Append(dependency2 != null ? dependency2.GetType().Name : "(null)"); + observedFeatures.Add(builder.ToString()); + Touch(); + } + } + + /// <summary> + /// Records statistics collected from incoming requests. + /// </summary> + /// <param name="request">The request.</param> + internal static void RecordRequestStatistics(HttpRequestInfo request) { + Contract.Requires(request != null); + + // In release builds, just quietly return. + if (request == null) { + return; + } + + if (Enabled) { + if (Configuration.IncludeCultures) { + observedCultures.Add(Thread.CurrentThread.CurrentCulture.Name); + } + + if (Configuration.IncludeLocalRequestUris && !observedRequests.IsFull) { + var requestBuilder = new UriBuilder(request.UrlBeforeRewriting); + requestBuilder.Query = null; + requestBuilder.Fragment = null; + observedRequests.Add(requestBuilder.Uri.AbsoluteUri); + } + + Touch(); + } + } + + /// <summary> + /// Called by every internal/public method on this class to give + /// periodic operations a chance to run. + /// </summary> + protected static void Touch() { + // Publish stats if it's time to do so. + lock (publishingConsiderationLock) { + if (DateTime.Now - lastPublished > Configuration.MinimumReportingInterval) { + lastPublished = DateTime.Now; + SendStatsAsync(); + } + } + } + + /// <summary> + /// Initializes Reporting if it has not been initialized yet. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method must never throw.")] + private static void Initialize() { + lock (initializationSync) { + if (!broken && !initialized) { + try { + file = GetIsolatedStorage(); + reportOriginIdentity = GetOrCreateOriginIdentity(); + + webRequestHandler = new StandardWebRequestHandler(); + observations.Add(observedRequests = new PersistentHashSet(file, "requests.txt", 3)); + observations.Add(observedCultures = new PersistentHashSet(file, "cultures.txt", 20)); + observations.Add(observedFeatures = new PersistentHashSet(file, "features.txt", int.MaxValue)); + + // Record site-wide features in use. + if (HttpContext.Current != null && HttpContext.Current.ApplicationInstance != null) { + // MVC or web forms? + // front-end or back end web farm? + // url rewriting? + ////RecordFeatureUse(IsMVC ? "ASP.NET MVC" : "ASP.NET Web Forms"); + } + + initialized = true; + } catch (Exception e) { + // This is supposed to be as low-risk as possible, so if it fails, just disable reporting + // and avoid rethrowing. + broken = true; + Logger.Library.Error("Error while trying to initialize reporting.", e); + } + } + } + } + + /// <summary> + /// Assembles a report for submission. + /// </summary> + /// <returns>A stream that contains the report.</returns> + private static Stream GetReport() { + var stream = new MemoryStream(); + try { + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine(reportOriginIdentity.ToString("B")); + writer.WriteLine(Util.LibraryVersion); + writer.WriteLine(".NET Framework {0}", Environment.Version); + + foreach (var observation in observations) { + observation.Flush(); + writer.WriteLine("===================================="); + writer.WriteLine(observation.FileName); + try { + using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } catch (FileNotFoundException) { + writer.WriteLine("(missing)"); + } + } + + // Not all event counters may have even loaded in this app instance. + // We flush the ones in memory, and then read all of them off disk. + foreach (var counter in events.Values) { + counter.Flush(); + } + + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + writer.WriteLine("===================================="); + writer.WriteLine(eventFile); + using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } + + // Make sure the stream is positioned at the beginning. + writer.Flush(); + stream.Position = 0; + return stream; + } catch { + stream.Dispose(); + throw; + } + } + + /// <summary> + /// Sends the usage reports to the library authors. + /// </summary> + /// <returns>A value indicating whether submitting the report was successful.</returns> + private static bool SendStats() { + try { + var request = (HttpWebRequest)WebRequest.Create(wellKnownPostLocation); + request.UserAgent = Util.LibraryVersion; + request.AllowAutoRedirect = false; + request.Method = "POST"; + request.ContentType = "text/dnoa-report1"; + Stream report = GetReport(); + request.ContentLength = report.Length; + using (var requestStream = webRequestHandler.GetRequestStream(request)) { + report.CopyTo(requestStream); + } + + using (var response = webRequestHandler.GetResponse(request)) { + Logger.Library.Info("Statistical report submitted successfully."); + + // The response stream may contain a message for the webmaster. + // Since as part of the report we submit the library version number, + // the report receiving service may have alerts such as: + // "You're using an obsolete version with exploitable security vulnerabilities." + using (var responseReader = response.GetResponseReader()) { + string line = responseReader.ReadLine(); + if (line != null) { + DemuxLogMessage(line); + } + } + } + + // Report submission was successful. Reset all counters. + lock (events) { + foreach (PersistentCounter counter in events.Values) { + counter.Reset(); + counter.Flush(); + } + + // We can just delete the files for counters that are not currently loaded. + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + if (!events.Values.Any(e => string.Equals(e.FileName, eventFile, StringComparison.OrdinalIgnoreCase))) { + file.DeleteFile(eventFile); + } + } + } + + return true; + } catch (ProtocolException ex) { + Logger.Library.Error("Unable to submit statistical report due to an HTTP error.", ex); + } catch (FileNotFoundException ex) { + Logger.Library.Error("Unable to submit statistical report because the report file is missing.", ex); + } + + return false; + } + + /// <summary> + /// Interprets the reporting response as a log message if possible. + /// </summary> + /// <param name="line">The line from the HTTP response to interpret as a log message.</param> + private static void DemuxLogMessage(string line) { + if (line != null) { + string[] parts = line.Split(new char[] { ' ' }, 2); + if (parts.Length == 2) { + string level = parts[0]; + string message = parts[1]; + switch (level) { + case "INFO": + Logger.Library.Info(message); + break; + case "WARN": + Logger.Library.Warn(message); + break; + case "ERROR": + Logger.Library.Error(message); + break; + case "FATAL": + Logger.Library.Fatal(message); + break; + } + } + } + } + + /// <summary> + /// Sends the stats report asynchronously, and careful to not throw any unhandled exceptions. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Unhandled exceptions MUST NOT be thrown from here.")] + private static void SendStatsAsync() { + // Do it on a background thread since it could take a while and we + // don't want to slow down this request we're borrowing. + ThreadPool.QueueUserWorkItem(state => { + try { + SendStats(); + } catch (Exception ex) { + // Something bad and unexpected happened. Just deactivate to avoid more trouble. + Logger.Library.Error("Error while trying to submit statistical report.", ex); + broken = true; + } + }); + } + + /// <summary> + /// Gets the isolated storage to use for reporting. + /// </summary> + /// <returns>An isolated storage location appropriate for our host.</returns> + private static IsolatedStorageFile GetIsolatedStorage() { + Contract.Ensures(Contract.Result<IsolatedStorageFile>() != null); + + IsolatedStorageFile result = null; + + // We'll try for whatever storage location we can get, + // and not catch exceptions from the last attempt so that + // the overall failure is caught by our caller. + try { + // This works on Personal Web Server + result = IsolatedStorageFile.GetUserStoreForDomain(); + } catch (SecurityException) { + } catch (IsolatedStorageException) { + } + + // This works on IIS when full trust is granted. + if (result == null) { + result = IsolatedStorageFile.GetMachineStoreForDomain(); + } + + Logger.Library.InfoFormat("Reporting will use isolated storage with scope: {0}", result.Scope); + return result; + } + + /// <summary> + /// Gets a unique, pseudonymous identifier for this particular web site or application. + /// </summary> + /// <returns>A GUID that will serve as the identifier.</returns> + /// <remarks> + /// The identifier is made persistent by storing the identifier in isolated storage. + /// If an existing identifier is not found, a new one is created, persisted, and returned. + /// </remarks> + private static Guid GetOrCreateOriginIdentity() { + Requires.ValidState(file != null); + Contract.Ensures(Contract.Result<Guid>() != Guid.Empty); + + Guid identityGuid = Guid.Empty; + const int GuidLength = 16; + using (var identityFileStream = new IsolatedStorageFileStream("identity.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, file)) { + if (identityFileStream.Length == GuidLength) { + byte[] guidBytes = new byte[GuidLength]; + if (identityFileStream.Read(guidBytes, 0, GuidLength) == GuidLength) { + identityGuid = new Guid(guidBytes); + } + } + + if (identityGuid == Guid.Empty) { + identityGuid = Guid.NewGuid(); + byte[] guidBytes = identityGuid.ToByteArray(); + identityFileStream.SetLength(0); + identityFileStream.Write(guidBytes, 0, guidBytes.Length); + } + + return identityGuid; + } + } + + /// <summary> + /// Sanitizes the name of the file so it only includes valid filename characters. + /// </summary> + /// <param name="fileName">The filename to sanitize.</param> + /// <returns>The filename, with any and all invalid filename characters replaced with the hyphen (-) character.</returns> + private static string SanitizeFileName(string fileName) { + Requires.NotNullOrEmpty(fileName, "fileName"); + char[] invalidCharacters = Path.GetInvalidFileNameChars(); + if (fileName.IndexOfAny(invalidCharacters) < 0) { + return fileName; // nothing invalid about this filename. + } + + // Use a stringbuilder since we may be replacing several characters + // and we don't want to instantiate a new string buffer for each new version. + StringBuilder sanitized = new StringBuilder(fileName); + foreach (char invalidChar in invalidCharacters) { + sanitized.Replace(invalidChar, '-'); + } + + return sanitized.ToString(); + } + + /// <summary> + /// A set of values that persist the set to disk. + /// </summary> + internal class PersistentHashSet : IDisposable { + /// <summary> + /// The isolated persistent storage. + /// </summary> + private readonly FileStream fileStream; + + /// <summary> + /// The persistent reader. + /// </summary> + private readonly StreamReader reader; + + /// <summary> + /// The persistent writer. + /// </summary> + private readonly StreamWriter writer; + + /// <summary> + /// The total set of elements. + /// </summary> + private readonly HashSet<string> memorySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// The maximum number of elements to track before not storing new elements. + /// </summary> + private readonly int maximumElements; + + /// <summary> + /// The set of new elements added to the <see cref="memorySet"/> since the last flush. + /// </summary> + private List<string> newElements = new List<string>(); + + /// <summary> + /// The time the last flush occurred. + /// </summary> + private DateTime lastFlushed; + + /// <summary> + /// A flag indicating whether the set has changed since it was last flushed. + /// </summary> + private bool dirty; + + /// <summary> + /// Initializes a new instance of the <see cref="PersistentHashSet"/> class. + /// </summary> + /// <param name="storage">The storage location.</param> + /// <param name="fileName">Name of the file.</param> + /// <param name="maximumElements">The maximum number of elements to track.</param> + internal PersistentHashSet(IsolatedStorageFile storage, string fileName, int maximumElements) { + Requires.NotNull(storage, "storage"); + Requires.NotNullOrEmpty(fileName, "fileName"); + this.FileName = fileName; + this.maximumElements = maximumElements; + + // Load the file into memory. + this.fileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, storage); + this.reader = new StreamReader(this.fileStream, Encoding.UTF8); + while (!this.reader.EndOfStream) { + this.memorySet.Add(this.reader.ReadLine()); + } + + this.writer = new StreamWriter(this.fileStream, Utf8NoPreamble); + this.lastFlushed = DateTime.Now; + } + + /// <summary> + /// Gets a value indicating whether the hashset has reached capacity and is not storing more elements. + /// </summary> + /// <value><c>true</c> if this instance is full; otherwise, <c>false</c>.</value> + internal bool IsFull { + get { + lock (this.memorySet) { + return this.memorySet.Count >= this.maximumElements; + } + } + } + + /// <summary> + /// Gets the name of the file. + /// </summary> + /// <value>The name of the file.</value> + internal string FileName { get; private set; } + + #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> + /// Adds a value to the set. + /// </summary> + /// <param name="value">The value.</param> + internal void Add(string value) { + lock (this.memorySet) { + if (!this.IsFull) { + if (this.memorySet.Add(value)) { + this.newElements.Add(value); + this.dirty = true; + + if (this.IsFull) { + this.Flush(); + } + } + + if (this.dirty && DateTime.Now - this.lastFlushed > Configuration.MinimumFlushInterval) { + this.Flush(); + } + } + } + } + + /// <summary> + /// Flushes any newly added values to disk. + /// </summary> + internal void Flush() { + lock (this.memorySet) { + foreach (string element in this.newElements) { + this.writer.WriteLine(element); + } + this.writer.Flush(); + + // Assign a whole new list since future lists might be smaller in order to + // decrease demand on memory. + this.newElements = new List<string>(); + this.dirty = false; + this.lastFlushed = DateTime.Now; + } + } + + /// <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.writer.Dispose(); + this.reader.Dispose(); + this.fileStream.Dispose(); + } + } + } + + /// <summary> + /// A feature usage counter. + /// </summary> + private class PersistentCounter : IDisposable { + /// <summary> + /// The separator to use between category names and their individual counters. + /// </summary> + private static readonly char[] separator = new char[] { '\t' }; + + /// <summary> + /// The isolated persistent storage. + /// </summary> + private readonly FileStream fileStream; + + /// <summary> + /// The persistent reader. + /// </summary> + private readonly StreamReader reader; + + /// <summary> + /// The persistent writer. + /// </summary> + private readonly StreamWriter writer; + + /// <summary> + /// The time the last flush occurred. + /// </summary> + private DateTime lastFlushed; + + /// <summary> + /// The in-memory copy of the counter. + /// </summary> + private Dictionary<string, int> counters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// A flag indicating whether the set has changed since it was last flushed. + /// </summary> + private bool dirty; + + /// <summary> + /// Initializes a new instance of the <see cref="PersistentCounter"/> class. + /// </summary> + /// <param name="storage">The storage location.</param> + /// <param name="fileName">Name of the file.</param> + internal PersistentCounter(IsolatedStorageFile storage, string fileName) { + Requires.NotNull(storage, "storage"); + Requires.NotNullOrEmpty(fileName, "fileName"); + this.FileName = fileName; + + // Load the file into memory. + this.fileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, storage); + this.reader = new StreamReader(this.fileStream, Encoding.UTF8); + while (!this.reader.EndOfStream) { + string line = this.reader.ReadLine(); + string[] parts = line.Split(separator, 2); + int counter; + if (int.TryParse(parts[0], out counter)) { + string category = string.Empty; + if (parts.Length > 1) { + category = parts[1]; + } + this.counters[category] = counter; + } + } + + this.writer = new StreamWriter(this.fileStream, Utf8NoPreamble); + this.lastFlushed = DateTime.Now; + } + + /// <summary> + /// Gets the name of the file. + /// </summary> + /// <value>The name of the file.</value> + internal string FileName { get; private set; } + + #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> + /// Increments the counter. + /// </summary> + /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + internal void Increment(string category) { + if (category == null) { + category = string.Empty; + } + lock (this) { + int counter; + this.counters.TryGetValue(category, out counter); + this.counters[category] = counter + 1; + this.dirty = true; + if (this.dirty && DateTime.Now - this.lastFlushed > Configuration.MinimumFlushInterval) { + this.Flush(); + } + } + } + + /// <summary> + /// Flushes any newly added values to disk. + /// </summary> + internal void Flush() { + lock (this) { + this.writer.BaseStream.Position = 0; + this.writer.BaseStream.SetLength(0); // truncate file + foreach (var pair in this.counters) { + this.writer.Write(pair.Value); + this.writer.Write(separator[0]); + this.writer.WriteLine(pair.Key); + } + this.writer.Flush(); + this.dirty = false; + this.lastFlushed = DateTime.Now; + } + } + + /// <summary> + /// Resets all counters. + /// </summary> + internal void Reset() { + lock (this) { + this.counters.Clear(); + } + } + + /// <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.writer.Dispose(); + this.reader.Dispose(); + this.fileStream.Dispose(); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Requires.cs b/src/DotNetOpenAuth.Core/Requires.cs new file mode 100644 index 0000000..4be6da0 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Requires.cs @@ -0,0 +1,250 @@ +//----------------------------------------------------------------------- +// <copyright file="Requires.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Argument validation checks that throw some kind of ArgumentException when they fail (unless otherwise noted). + /// </summary> + internal static class Requires { + /// <summary> + /// Validates that a given parameter is not null. + /// </summary> + /// <typeparam name="T">The type of the parameter</typeparam> + /// <param name="value">The value.</param> + /// <param name="parameterName">Name of the parameter.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void NotNull<T>(T value, string parameterName) where T : class { + if (value == null) { + throw new ArgumentNullException(parameterName); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates that a parameter is not null or empty. + /// </summary> + /// <param name="value">The value.</param> + /// <param name="parameterName">Name of the parameter.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void NotNullOrEmpty(string value, string parameterName) { + NotNull(value, parameterName); + True(value.Length > 0, parameterName, Strings.EmptyStringNotAllowed); + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates that an array is not null or empty. + /// </summary> + /// <typeparam name="T">The type of the elements in the sequence.</typeparam> + /// <param name="value">The value.</param> + /// <param name="parameterName">Name of the parameter.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void NotNullOrEmpty<T>(IEnumerable<T> value, string parameterName) { + NotNull(value, parameterName); + True(value.Any(), parameterName, Strings.InvalidArgument); + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates that an argument is either null or is a sequence with no null elements. + /// </summary> + /// <typeparam name="T">The type of elements in the sequence.</typeparam> + /// <param name="sequence">The sequence.</param> + /// <param name="parameterName">Name of the parameter.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void NullOrWithNoNullElements<T>(IEnumerable<T> sequence, string parameterName) where T : class { + if (sequence != null) { + if (sequence.Any(e => e == null)) { + throw new ArgumentException(MessagingStrings.SequenceContainsNullElement, parameterName); + } + } + } + + /// <summary> + /// Validates some expression describing the acceptable range for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="ArgumentOutOfRangeException"/>.</param> + /// <param name="parameterName">Name of the parameter.</param> + /// <param name="message">The message to include with the exception.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void InRange(bool condition, string parameterName, string message = null) { + if (!condition) { + throw new ArgumentOutOfRangeException(parameterName, message); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="ArgumentException"/>.</param> + /// <param name="parameterName">Name of the parameter.</param> + /// <param name="message">The message to include with the exception.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void True(bool condition, string parameterName = null, string message = null) { + if (!condition) { + throw new ArgumentException(message ?? Strings.InvalidArgument, parameterName); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="ArgumentException"/>.</param> + /// <param name="parameterName">Name of the parameter.</param> + /// <param name="unformattedMessage">The unformatted message.</param> + /// <param name="args">Formatting arguments.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void True(bool condition, string parameterName, string unformattedMessage, params object[] args) { + if (!condition) { + throw new ArgumentException(String.Format(unformattedMessage, args), parameterName); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="InvalidOperationException"/>.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void ValidState(bool condition) { + if (!condition) { + throw new InvalidOperationException(); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="InvalidOperationException"/>.</param> + /// <param name="message">The message to include with the exception.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void ValidState(bool condition, string message) { + if (!condition) { + throw new InvalidOperationException(message); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="InvalidOperationException"/>.</param> + /// <param name="unformattedMessage">The unformatted message.</param> + /// <param name="args">Formatting arguments.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void ValidState(bool condition, string unformattedMessage, params object[] args) { + if (!condition) { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, unformattedMessage, args)); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates that some argument describes a type that is or derives from a required type. + /// </summary> + /// <typeparam name="T">The type that the argument must be or derive from.</typeparam> + /// <param name="type">The type given in the argument.</param> + /// <param name="parameterName">Name of the parameter.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void NotNullSubtype<T>(Type type, string parameterName) { + NotNull(type, parameterName); + True(typeof(T).IsAssignableFrom(type), parameterName, MessagingStrings.UnexpectedType, typeof(T).FullName, type.FullName); + + Contract.EndContractBlock(); + } + + /// <summary> + /// Validates some expression describing the acceptable condition for an argument evaluates to true. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="FormatException"/>.</param> + /// <param name="message">The message.</param> +#if !CLR4 + [ContractArgumentValidator] +#endif + [Pure, DebuggerStepThrough] + internal static void Format(bool condition, string message) { + if (!condition) { + throw new FormatException(message); + } + + Contract.EndContractBlock(); + } + + /// <summary> + /// Throws an <see cref="NotSupportedException"/> if a condition does not evaluate to <c>true</c>. + /// </summary> + /// <param name="condition">The expression that must evaluate to true to avoid an <see cref="NotSupportedException"/>.</param> + /// <param name="message">The message.</param> + [Pure, DebuggerStepThrough] + internal static void Support(bool condition, string message) { + if (!condition) { + throw new NotSupportedException(message); + } + } + + /// <summary> + /// Throws an <see cref="ArgumentException"/> + /// </summary> + /// <param name="parameterName">Name of the parameter.</param> + /// <param name="message">The message.</param> + [Pure, DebuggerStepThrough] + internal static void Fail(string parameterName, string message) { + throw new ArgumentException(message, parameterName); + } + } +} diff --git a/src/DotNetOpenAuth.Core/Settings.StyleCop b/src/DotNetOpenAuth.Core/Settings.StyleCop new file mode 100644 index 0000000..017d610 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Settings.StyleCop @@ -0,0 +1,34 @@ +<StyleCopSettings Version="4.3"> + <Analyzers> + <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.DocumentationRules"> + <Rules> + <Rule Name="ElementDocumentationMustNotBeCopiedAndPasted"> + <RuleSettings> + <BooleanProperty Name="Enabled">False</BooleanProperty> + </RuleSettings> + </Rule> + </Rules> + <AnalyzerSettings /> + </Analyzer> + <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.MaintainabilityRules"> + <Rules> + <Rule Name="StatementMustNotUseUnnecessaryParenthesis"> + <RuleSettings> + <BooleanProperty Name="Enabled">False</BooleanProperty> + </RuleSettings> + </Rule> + </Rules> + <AnalyzerSettings /> + </Analyzer> + <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.ReadabilityRules"> + <Rules> + <Rule Name="UseStringEmptyForEmptyStrings"> + <RuleSettings> + <BooleanProperty Name="Enabled">False</BooleanProperty> + </RuleSettings> + </Rule> + </Rules> + <AnalyzerSettings /> + </Analyzer> + </Analyzers> +</StyleCopSettings>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Strings.Designer.cs b/src/DotNetOpenAuth.Core/Strings.Designer.cs new file mode 100644 index 0000000..21411a1 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Strings.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.17291 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth { + 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 Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <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.Strings", typeof(Strings).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 The configuration-specified type {0} must be public, and is not.. + /// </summary> + internal static string ConfigurationTypeMustBePublic { + get { + return ResourceManager.GetString("ConfigurationTypeMustBePublic", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The configuration XAML reference to {0} requires a current HttpContext to resolve.. + /// </summary> + internal static string ConfigurationXamlReferenceRequiresHttpContext { + get { + return ResourceManager.GetString("ConfigurationXamlReferenceRequiresHttpContext", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The current IHttpHandler is not one of types: {0}. An embedded resource URL provider must be set in your .config file.. + /// </summary> + internal static string EmbeddedResourceUrlProviderRequired { + get { + return ResourceManager.GetString("EmbeddedResourceUrlProviderRequired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The empty string is not allowed.. + /// </summary> + internal static string EmptyStringNotAllowed { + get { + return ResourceManager.GetString("EmptyStringNotAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The argument has an unexpected value.. + /// </summary> + internal static string InvalidArgument { + get { + return ResourceManager.GetString("InvalidArgument", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No current HttpContext was detected, so an {0} instance must be explicitly provided or specified in the .config file. Call the constructor overload that takes an {0}.. + /// </summary> + internal static string StoreRequiredWhenNoHttpContextAvailable { + get { + return ResourceManager.GetString("StoreRequiredWhenNoHttpContextAvailable", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Strings.resx b/src/DotNetOpenAuth.Core/Strings.resx new file mode 100644 index 0000000..1c69ef7 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Strings.resx @@ -0,0 +1,138 @@ +<?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="ConfigurationTypeMustBePublic" xml:space="preserve"> + <value>The configuration-specified type {0} must be public, and is not.</value> + </data> + <data name="StoreRequiredWhenNoHttpContextAvailable" xml:space="preserve"> + <value>No current HttpContext was detected, so an {0} instance must be explicitly provided or specified in the .config file. Call the constructor overload that takes an {0}.</value> + </data> + <data name="ConfigurationXamlReferenceRequiresHttpContext" xml:space="preserve"> + <value>The configuration XAML reference to {0} requires a current HttpContext to resolve.</value> + </data> + <data name="EmbeddedResourceUrlProviderRequired" xml:space="preserve"> + <value>The current IHttpHandler is not one of types: {0}. An embedded resource URL provider must be set in your .config file.</value> + </data> + <data name="EmptyStringNotAllowed" xml:space="preserve"> + <value>The empty string is not allowed.</value> + </data> + <data name="InvalidArgument" xml:space="preserve"> + <value>The argument has an unexpected value.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Strings.sr.resx b/src/DotNetOpenAuth.Core/Strings.sr.resx new file mode 100644 index 0000000..5112265 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Strings.sr.resx @@ -0,0 +1,126 @@ +<?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="ConfigurationTypeMustBePublic" xml:space="preserve"> + <value>Konfiguraciono zavisni tip {0} mora biti javan, a on to nije.</value> + </data> + <data name="ConfigurationXamlReferenceRequiresHttpContext" xml:space="preserve"> + <value>Konfiguraciona XAML referenca na {0} zahteva da se trenutni HttpContext razreši.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/UriUtil.cs b/src/DotNetOpenAuth.Core/UriUtil.cs new file mode 100644 index 0000000..57360f5 --- /dev/null +++ b/src/DotNetOpenAuth.Core/UriUtil.cs @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------- +// <copyright file="UriUtil.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.UI; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Utility methods for working with URIs. + /// </summary> + [ContractVerification(true)] + internal static class UriUtil { + /// <summary> + /// Tests a URI for the presence of an OAuth payload. + /// </summary> + /// <param name="uri">The URI to test.</param> + /// <param name="prefix">The prefix.</param> + /// <returns> + /// True if the URI contains an OAuth message. + /// </returns> + [ContractVerification(false)] // bugs/limitations in CC static analysis + internal static bool QueryStringContainPrefixedParameters(this Uri uri, string prefix) { + Requires.NotNullOrEmpty(prefix, "prefix"); + if (uri == null) { + return false; + } + + NameValueCollection nvc = HttpUtility.ParseQueryString(uri.Query); + Contract.Assume(nvc != null); // BCL + return nvc.Keys.OfType<string>().Any(key => key.StartsWith(prefix, StringComparison.Ordinal)); + } + + /// <summary> + /// Determines whether some <see cref="Uri"/> is using HTTPS. + /// </summary> + /// <param name="uri">The Uri being tested for security.</param> + /// <returns> + /// <c>true</c> if the URI represents an encrypted request; otherwise, <c>false</c>. + /// </returns> + internal static bool IsTransportSecure(this Uri uri) { + Requires.NotNull(uri, "uri"); + return string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Equivalent to UriBuilder.ToString() but omits port # if it may be implied. + /// Equivalent to UriBuilder.Uri.ToString(), but doesn't throw an exception if the Host has a wildcard. + /// </summary> + /// <param name="builder">The UriBuilder to render as a string.</param> + /// <returns>The string version of the Uri.</returns> + internal static string ToStringWithImpliedPorts(this UriBuilder builder) { + Requires.NotNull(builder, "builder"); + Contract.Ensures(Contract.Result<string>() != null); + + // We only check for implied ports on HTTP and HTTPS schemes since those + // are the only ones supported by OpenID anyway. + if ((builder.Port == 80 && string.Equals(builder.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || + (builder.Port == 443 && string.Equals(builder.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { + // An implied port may be removed. + string url = builder.ToString(); + + // Be really careful to only remove the first :80 or :443 so we are guaranteed + // we're removing only the port (and not something in the query string that + // looks like a port. + string result = Regex.Replace(url, @"^(https?://[^:]+):\d+", m => m.Groups[1].Value, RegexOptions.IgnoreCase); + Contract.Assume(result != null); // Regex.Replace never returns null + return result; + } else { + // The port must be explicitly given anyway. + return builder.ToString(); + } + } + + /// <summary> + /// Validates that a URL will be resolvable at runtime. + /// </summary> + /// <param name="page">The page hosting the control that receives this URL as a property.</param> + /// <param name="designMode">If set to <c>true</c> the page is in design-time mode rather than runtime mode.</param> + /// <param name="value">The URI to check.</param> + /// <exception cref="UriFormatException">Thrown if the given URL is not a valid, resolvable URI.</exception> + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Just to throw an exception on invalid input.")] + internal static void ValidateResolvableUrl(Page page, bool designMode, string value) { + if (string.IsNullOrEmpty(value)) { + return; + } + + if (page != null && !designMode) { + Contract.Assume(page.Request != null); + + // Validate new value by trying to construct a Realm object based on it. + string relativeUrl = page.ResolveUrl(value); + Contract.Assume(page.Request.Url != null); + Contract.Assume(relativeUrl != null); + new Uri(page.Request.Url, relativeUrl); // throws an exception on failure. + } else { + // We can't fully test it, but it should start with either ~/ or a protocol. + if (Regex.IsMatch(value, @"^https?://")) { + new Uri(value); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.Core/Util.cs b/src/DotNetOpenAuth.Core/Util.cs new file mode 100644 index 0000000..6b63a36 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Util.cs @@ -0,0 +1,229 @@ +//----------------------------------------------------------------------- +// <copyright file="Util.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- +namespace DotNetOpenAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Net; + using System.Reflection; + using System.Text; + using System.Web; + using System.Web.UI; + + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A grab-bag utility class. + /// </summary> + [ContractVerification(true)] + internal static class Util { + /// <summary> + /// The base namespace for this library from which all other namespaces derive. + /// </summary> + internal const string DefaultNamespace = "DotNetOpenAuth"; + + /// <summary> + /// The web.config file-specified provider of web resource URLs. + /// </summary> + private static IEmbeddedResourceRetrieval embeddedResourceRetrieval = MessagingElement.Configuration.EmbeddedResourceRetrievalProvider.CreateInstance(null, false); + + /// <summary> + /// Gets a human-readable description of the library name and version, including + /// whether the build is an official or private one. + /// </summary> + public static string LibraryVersion { + get { + string assemblyFullName = Assembly.GetExecutingAssembly().FullName; + bool official = assemblyFullName.Contains("PublicKeyToken=2780ccd10d57b246"); + + // We use InvariantCulture since this is used for logging. + return string.Format(CultureInfo.InvariantCulture, "{0} ({1})", assemblyFullName, official ? "official" : "private"); + } + } + + /// <summary> + /// Tests for equality between two objects. Safely handles the case where one or both are null. + /// </summary> + /// <typeparam name="T">The type of objects been checked for equality.</typeparam> + /// <param name="first">The first object.</param> + /// <param name="second">The second object.</param> + /// <returns><c>true</c> if the two objects are equal; <c>false</c> otherwise.</returns> + internal static bool EqualsNullSafe<T>(this T first, T second) where T : class { + // If one is null and the other is not... + if (object.ReferenceEquals(first, null) ^ object.ReferenceEquals(second, null)) { + return false; + } + + // If both are null... (we only check one because we already know both are either null or non-null) + if (object.ReferenceEquals(first, null)) { + return true; + } + + // Neither are null. Delegate to the Equals method. + return first.Equals(second); + } + + /// <summary> + /// Prepares a dictionary for printing as a string. + /// </summary> + /// <typeparam name="K">The type of the key.</typeparam> + /// <typeparam name="V">The type of the value.</typeparam> + /// <param name="pairs">The dictionary or sequence of name-value pairs.</param> + /// <returns>An object whose ToString method will perform the actual work of generating the string.</returns> + /// <remarks> + /// The work isn't done until (and if) the + /// <see cref="Object.ToString"/> method is actually called, which makes it great + /// for logging complex objects without being in a conditional block. + /// </remarks> + internal static object ToStringDeferred<K, V>(this IEnumerable<KeyValuePair<K, V>> pairs) { + return new DelayedToString<IEnumerable<KeyValuePair<K, V>>>( + pairs, + p => { + ////Contract.Requires(pairs != null); // CC: anonymous method can't handle it + ErrorUtilities.VerifyArgumentNotNull(pairs, "pairs"); + var dictionary = pairs as IDictionary<K, V>; + StringBuilder sb = new StringBuilder(dictionary != null ? dictionary.Count * 40 : 200); + foreach (var pair in pairs) { + sb.AppendFormat("\t{0}: {1}{2}", pair.Key, pair.Value, Environment.NewLine); + } + return sb.ToString(); + }); + } + + /// <summary> + /// Offers deferred ToString processing for a list of elements, that are assumed + /// to generate just a single-line string. + /// </summary> + /// <typeparam name="T">The type of elements contained in the list.</typeparam> + /// <param name="list">The list of elements.</param> + /// <returns>An object whose ToString method will perform the actual work of generating the string.</returns> + internal static object ToStringDeferred<T>(this IEnumerable<T> list) { + return ToStringDeferred<T>(list, false); + } + + /// <summary> + /// Offers deferred ToString processing for a list of elements. + /// </summary> + /// <typeparam name="T">The type of elements contained in the list.</typeparam> + /// <param name="list">The list of elements.</param> + /// <param name="multiLineElements">if set to <c>true</c>, special formatting will be applied to the output to make it clear where one element ends and the next begins.</param> + /// <returns>An object whose ToString method will perform the actual work of generating the string.</returns> + [ContractVerification(false)] + internal static object ToStringDeferred<T>(this IEnumerable<T> list, bool multiLineElements) { + return new DelayedToString<IEnumerable<T>>( + list, + l => { + // Code contracts not allowed in generator methods. + ErrorUtilities.VerifyArgumentNotNull(l, "l"); + + string newLine = Environment.NewLine; + ////Contract.Assume(newLine != null && newLine.Length > 0); + StringBuilder sb = new StringBuilder(); + if (multiLineElements) { + sb.AppendLine("[{"); + foreach (T obj in l) { + // Prepare the string repersentation of the object + string objString = obj != null ? obj.ToString() : "<NULL>"; + + // Indent every line printed + objString = objString.Replace(newLine, Environment.NewLine + "\t"); + sb.Append("\t"); + sb.Append(objString); + + if (!objString.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { + sb.AppendLine(); + } + sb.AppendLine("}, {"); + } + if (sb.Length > 2 + Environment.NewLine.Length) { // if anything was in the enumeration + sb.Length -= 2 + Environment.NewLine.Length; // trim off the last ", {\r\n" + } else { + sb.Length -= 1 + Environment.NewLine.Length; // trim off the opening { + } + sb.Append("]"); + return sb.ToString(); + } else { + sb.Append("{"); + foreach (T obj in l) { + sb.Append(obj != null ? obj.ToString() : "<NULL>"); + sb.AppendLine(","); + } + if (sb.Length > 1) { + sb.Length -= 1; + } + sb.Append("}"); + return sb.ToString(); + } + }); + } + + /// <summary> + /// Gets the web resource URL from a Page or <see cref="IEmbeddedResourceRetrieval"/> object. + /// </summary> + /// <param name="someTypeInResourceAssembly">Some type in resource assembly.</param> + /// <param name="manifestResourceName">Name of the manifest resource.</param> + /// <returns>An absolute URL</returns> + internal static string GetWebResourceUrl(Type someTypeInResourceAssembly, string manifestResourceName) { + Page page; + IEmbeddedResourceRetrieval retrieval; + + if (embeddedResourceRetrieval != null) { + Uri url = embeddedResourceRetrieval.GetWebResourceUrl(someTypeInResourceAssembly, manifestResourceName); + return url != null ? url.AbsoluteUri : null; + } else if ((page = HttpContext.Current.CurrentHandler as Page) != null) { + return page.ClientScript.GetWebResourceUrl(someTypeInResourceAssembly, manifestResourceName); + } else if ((retrieval = HttpContext.Current.CurrentHandler as IEmbeddedResourceRetrieval) != null) { + return retrieval.GetWebResourceUrl(someTypeInResourceAssembly, manifestResourceName).AbsoluteUri; + } else { + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Strings.EmbeddedResourceUrlProviderRequired, + string.Join(", ", new string[] { typeof(Page).FullName, typeof(IEmbeddedResourceRetrieval).FullName }))); + } + } + + /// <summary> + /// Manages an individual deferred ToString call. + /// </summary> + /// <typeparam name="T">The type of object to be serialized as a string.</typeparam> + private class DelayedToString<T> { + /// <summary> + /// The object that will be serialized if called upon. + /// </summary> + private readonly T obj; + + /// <summary> + /// The method used to serialize <see cref="obj"/> to string form. + /// </summary> + private readonly Func<T, string> toString; + + /// <summary> + /// Initializes a new instance of the DelayedToString class. + /// </summary> + /// <param name="obj">The object that may be serialized to string form.</param> + /// <param name="toString">The method that will serialize the object if called upon.</param> + public DelayedToString(T obj, Func<T, string> toString) { + Requires.NotNull(toString, "toString"); + + this.obj = obj; + this.toString = toString; + } + + /// <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() { + return this.toString(this.obj) ?? string.Empty; + } + } + } +} |