diff options
Diffstat (limited to 'src')
48 files changed, 1800 insertions, 120 deletions
diff --git a/src/DotNetOpenAuth.BuildTasks/CompareFiles.cs b/src/DotNetOpenAuth.BuildTasks/CompareFiles.cs index 691df20..51fcee4 100644 --- a/src/DotNetOpenAuth.BuildTasks/CompareFiles.cs +++ b/src/DotNetOpenAuth.BuildTasks/CompareFiles.cs @@ -81,5 +81,32 @@ namespace DotNetOpenAuth.BuildTasks { return true; } + + /// <summary> + /// Tests whether a file is up to date with respect to another, + /// based on existence, last write time and file size. + /// </summary> + /// <param name="sourcePath">The source path.</param> + /// <param name="destPath">The dest path.</param> + /// <returns><c>true</c> if the files are the same; <c>false</c> if the files are different</returns> + internal static bool FastFileEqualityCheck(string sourcePath, string destPath) { + FileInfo sourceInfo = new FileInfo(sourcePath); + FileInfo destInfo = new FileInfo(destPath); + + if (sourceInfo.Exists ^ destInfo.Exists) { + // Either the source file or the destination file is missing. + return false; + } + + if (!sourceInfo.Exists) { + // Neither file exists. + return true; + } + + // We'll say the files are the same if their modification date and length are the same. + return + sourceInfo.LastWriteTimeUtc == destInfo.LastWriteTimeUtc && + sourceInfo.Length == destInfo.Length; + } } } diff --git a/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs b/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs index 3b81978..e17d8f2 100644 --- a/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs +++ b/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs @@ -18,15 +18,6 @@ namespace DotNetOpenAuth.BuildTasks { /// </summary> public class CopyWithTokenSubstitution : Task { /// <summary> - /// Gets or sets a value indicating whether the task should - /// skip the copying of files that are unchanged between the source and destination. - /// </summary> - /// <value> - /// <c>true</c> to skip copying files where the destination files are newer than the source files; otherwise, <c>false</c> to copy all files. - /// </value> - public bool SkipUnchangedFiles { get; set; } - - /// <summary> /// Gets or sets the files to copy. /// </summary> /// <value>The files to copy.</value> @@ -65,8 +56,11 @@ namespace DotNetOpenAuth.BuildTasks { for (int i = 0; i < this.SourceFiles.Length; i++) { string sourcePath = this.SourceFiles[i].ItemSpec; string destPath = this.DestinationFiles[i].ItemSpec; + bool skipUnchangedFiles = bool.Parse(this.SourceFiles[i].GetMetadata("SkipUnchangedFiles")); - if (this.SkipUnchangedFiles && File.GetLastWriteTimeUtc(sourcePath) < File.GetLastWriteTimeUtc(destPath)) { + // We deliberably consider newer destination files to be up-to-date rather than + // requiring equality because this task modifies the destination file while copying. + if (skipUnchangedFiles && File.GetLastWriteTimeUtc(sourcePath) < File.GetLastWriteTimeUtc(destPath)) { Log.LogMessage(MessageImportance.Low, "Skipping \"{0}\" -> \"{1}\" because the destination is up to date.", sourcePath, destPath); continue; } diff --git a/src/DotNetOpenAuth.Test/App.config b/src/DotNetOpenAuth.Test/App.config index dafb99d..b3da723 100644 --- a/src/DotNetOpenAuth.Test/App.config +++ b/src/DotNetOpenAuth.Test/App.config @@ -49,6 +49,8 @@ </security> </provider> </openid> + <!-- We definitely do NOT want to report on events that happen while running tests. --> + <reporting enabled="false" /> </dotNetOpenAuth> <system.diagnostics> diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index b9cb6b1..18e1c5c 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -4,28 +4,68 @@ 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: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: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> @@ -55,19 +95,42 @@ </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: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: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> @@ -97,27 +160,92 @@ </xs:complexType> </xs:element> </xs:choice> - <xs:attribute name="timeout" type="xs:string" /> - <xs:attribute name="readWriteTimeout" type="xs:string" /> - <xs:attribute name="maximumBytesToRead" type="xs:int" /> - <xs:attribute name="maximumRedirections" type="xs:int" /> + <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:choice> - <xs:attribute name="lifetime" type="xs:string" /> - <xs:attribute name="clockSkew" type="xs:string" /> + <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: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:attribute name="requireSsl" type="xs:boolean" default="false" /> + <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" /> @@ -126,29 +254,104 @@ </xs:restriction> </xs:simpleType> </xs:attribute> - <xs:attribute name="minimumHashBitLength" type="xs:int" /> - <xs:attribute name="maximumHashBitLength" type="xs:int" /> - <xs:attribute name="privateSecretMaximumAge" type="xs:string" /> - <xs:attribute name="requireDirectedIdentity" type="xs:boolean" /> - <xs:attribute name="requireAssociation" type="xs:boolean" /> - <xs:attribute name="rejectUnsolicitedAssertions" type="xs:boolean" /> - <xs:attribute name="rejectDelegatingIdentifiers" type="xs:boolean" /> - <xs:attribute name="ignoreUnsignedExtensions" type="xs:boolean" /> + <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="privateSecretMaximumAge" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum age of a secret used for private signing before it is renewed. + </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: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: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: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"> @@ -181,31 +384,76 @@ </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: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: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:attribute name="lifetime" type="xs:string" use="required" /> + <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: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"> @@ -217,27 +465,92 @@ </xs:complexType> </xs:element> </xs:choice> - <xs:attribute name="requireSsl" type="xs:boolean" default="false" /> - <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean" /> + <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="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:enumeration value="LogWarningOnFailure" /> - <xs:enumeration value="NeverVerify" /> + <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:attribute name="maximumHashBitLength" type="xs:int" /> + <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: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> @@ -255,25 +568,54 @@ </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: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: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: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"> @@ -285,22 +627,61 @@ </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:attribute name="proxy" type="xs:string" /> + <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: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: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> @@ -309,24 +690,70 @@ </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:enumeration value="V10a" /> + <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: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: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> @@ -335,6 +762,70 @@ </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> diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs index 7bd84d9..aa956d1 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs @@ -35,6 +35,11 @@ namespace DotNetOpenAuth.Configuration { private const string OAuthElementName = "oauth"; /// <summary> + /// The name of the <reporting> sub-element. + /// </summary> + private const string ReportingElementName = "reporting"; + + /// <summary> /// Initializes a new instance of the <see cref="DotNetOpenAuthSection"/> class. /// </summary> internal DotNetOpenAuthSection() { @@ -75,5 +80,14 @@ namespace DotNetOpenAuth.Configuration { get { return (OAuthElement)this[OAuthElementName] ?? new OAuthElement(); } set { this[OAuthElementName] = value; } } + + /// <summary> + /// Gets or sets the configuration for reporting. + /// </summary> + [ConfigurationProperty(ReportingElementName)] + internal ReportingElement Reporting { + get { return (ReportingElement)this[ReportingElementName] ?? new ReportingElement(); } + set { this[ReportingElementName] = value; } + } } } diff --git a/src/DotNetOpenAuth/Configuration/OpenIdElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdElement.cs index 404b2f6..69994e6 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdElement.cs @@ -113,10 +113,10 @@ namespace DotNetOpenAuth.Configuration { } /// <summary> - /// Gets or sets the registered OpenID extensions. + /// Gets or sets the registered OpenID extension factories. /// </summary> [ConfigurationProperty(ExtensionFactoriesElementName, IsDefaultCollection = false)] - [ConfigurationCollection(typeof(TypeConfigurationCollection<IOpenIdMessageExtension>))] + [ConfigurationCollection(typeof(TypeConfigurationCollection<IOpenIdExtensionFactory>))] internal TypeConfigurationCollection<IOpenIdExtensionFactory> ExtensionFactories { get { return (TypeConfigurationCollection<IOpenIdExtensionFactory>)this[ExtensionFactoriesElementName] ?? new TypeConfigurationCollection<IOpenIdExtensionFactory>(); } set { this[ExtensionFactoriesElementName] = value; } diff --git a/src/DotNetOpenAuth/Configuration/ReportingElement.cs b/src/DotNetOpenAuth/Configuration/ReportingElement.cs new file mode 100644 index 0000000..ab1437f --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/ReportingElement.cs @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------- +// <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.Linq; + using System.Text; + + /// <summary> + /// Represents the <reporting> element in the host's .config file. + /// </summary> + internal class ReportingElement : ConfigurationElement { + /// <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 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 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 = false)] + 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/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index d3125e3..053861a 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -230,6 +230,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="Configuration\OpenIdProviderSecuritySettingsElement.cs" /> <Compile Include="Configuration\OpenIdRelyingPartyElement.cs" /> <Compile Include="Configuration\OpenIdRelyingPartySecuritySettingsElement.cs" /> + <Compile Include="Configuration\ReportingElement.cs" /> <Compile Include="Configuration\TypeConfigurationCollection.cs" /> <Compile Include="Configuration\TypeConfigurationElement.cs" /> <Compile Include="Configuration\UntrustedWebRequestElement.cs" /> @@ -559,6 +560,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OAuth\ChannelElements\RsaSha1SigningBindingElement.cs" /> <Compile Include="Messaging\StandardWebRequestHandler.cs" /> <Compile Include="Messaging\MessageReceivingEndpoint.cs" /> + <Compile Include="Reporting.cs" /> <Compile Include="Util.cs" /> <Compile Include="OAuth\Protocol.cs" /> <Compile Include="OAuth\ServiceProvider.cs" /> diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs index de4d023..86c1118 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs @@ -199,6 +199,7 @@ namespace DotNetOpenAuth.InfoCard { /// </summary> public InfoCardSelector() { this.ToolTip = InfoCardStrings.SelectorClickPrompt; + Reporting.RecordFeatureUse(this); } /// <summary> diff --git a/src/DotNetOpenAuth/Logger.cs b/src/DotNetOpenAuth/Logger.cs index a9dbef2..48007ed 100644 --- a/src/DotNetOpenAuth/Logger.cs +++ b/src/DotNetOpenAuth/Logger.cs @@ -60,6 +60,11 @@ namespace DotNetOpenAuth { 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"); @@ -110,6 +115,11 @@ namespace DotNetOpenAuth { 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; } } diff --git a/src/DotNetOpenAuth/Loggers/ILog.cs b/src/DotNetOpenAuth/Loggers/ILog.cs index 4ddbd49..8094296 100644 --- a/src/DotNetOpenAuth/Loggers/ILog.cs +++ b/src/DotNetOpenAuth/Loggers/ILog.cs @@ -21,7 +21,7 @@ // 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 -// dotnetopenid.dll. +// DotNetOpenAuth.dll. namespace DotNetOpenAuth.Loggers { diff --git a/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs b/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs index 0a7ddbd..bb56cfd 100644 --- a/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs +++ b/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs @@ -125,6 +125,7 @@ namespace DotNetOpenAuth.Messaging.Bindings { 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); } diff --git a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs index 2951514..6c7f2f9 100644 --- a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs @@ -73,6 +73,8 @@ namespace DotNetOpenAuth.Messaging { // these as well. this.form = request.Form; this.queryString = request.QueryString; + + Reporting.RecordRequestStatistics(this); } /// <summary> @@ -96,6 +98,8 @@ namespace DotNetOpenAuth.Messaging { this.RawUrl = rawUrl; this.Headers = headers; this.InputStream = inputStream; + + Reporting.RecordRequestStatistics(this); } /// <summary> @@ -115,6 +119,8 @@ namespace DotNetOpenAuth.Messaging { } this.InputStream = listenerRequest.InputStream; + + Reporting.RecordRequestStatistics(this); } /// <summary> @@ -131,6 +137,8 @@ namespace DotNetOpenAuth.Messaging { this.Url = requestUri; this.UrlBeforeRewriting = requestUri; this.RawUrl = MakeUpRawUrlFromUrl(requestUri); + + Reporting.RecordRequestStatistics(this); } /// <summary> @@ -157,6 +165,8 @@ namespace DotNetOpenAuth.Messaging { this.RawUrl = MakeUpRawUrlFromUrl(request.RequestUri); this.Headers = GetHeaderCollection(request.Headers); this.InputStream = null; + + Reporting.RecordRequestStatistics(this); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index e1e3f59..3f77a25 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -152,6 +152,7 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<ArgumentNullException>(requestHandler != null); Contract.Requires<ArgumentNullException>(parts != null); + Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart"); string boundary = Guid.NewGuid().ToString(); string partLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}\r\n", boundary); string finalTrailingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}--\r\n", boundary); diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs index b9d4718..6c0ce42 100644 --- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs @@ -34,6 +34,8 @@ namespace DotNetOpenAuth.OAuth { this.OAuthChannel = new OAuthChannel(signingElement, store, tokenManager); this.ServiceProvider = serviceDescription; this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.Consumer.SecuritySettings.CreateSecuritySettings(); + + Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, null); } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs index e2c82bb..829b572 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs @@ -102,6 +102,8 @@ namespace DotNetOpenAuth.OAuth { this.OAuthChannel = new OAuthChannel(signingElement, nonceStore, tokenManager, messageTypeProvider); this.TokenGenerator = new StandardTokenGenerator(); this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.SecuritySettings.CreateSecuritySettings(); + + Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, nonceStore); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs b/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs index 23377c8..20c207f 100644 --- a/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs +++ b/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs @@ -114,6 +114,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors { !requestInternal.AppliedExtensions.OfType<FetchRequest>().Any()), BehaviorStrings.PiiIncludedWithNoPiiPolicy); } + + Reporting.RecordEventOccurrence(this, "RP"); } /// <summary> @@ -256,6 +258,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors { } } } + + Reporting.RecordEventOccurrence(this, "OP"); } return result; diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs b/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs index baef943..a465611 100644 --- a/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs +++ b/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs @@ -108,6 +108,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors { if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier); } + + Reporting.RecordEventOccurrence(this, string.Empty); } } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs index 84adc59..4fa70a0 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs @@ -88,6 +88,8 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { foreach (IExtensionMessage protocolExtension in extendableMessage.Extensions) { var extension = protocolExtension as IOpenIdMessageExtension; if (extension != null) { + Reporting.RecordFeatureUse(protocolExtension); + // Give extensions that require custom serialization a chance to do their work. var customSerializingExtension = extension as IMessageWithEvents; if (customSerializingExtension != null) { @@ -145,6 +147,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { if (extendableMessage != null) { // First add the extensions that are signed by the Provider. foreach (IOpenIdMessageExtension signedExtension in this.GetExtensions(extendableMessage, true, null)) { + Reporting.RecordFeatureUse(signedExtension); signedExtension.IsSignedByRemoteParty = true; extendableMessage.Extensions.Add(signedExtension); } @@ -154,6 +157,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { if (this.relyingPartySecuritySettings == null || !this.relyingPartySecuritySettings.IgnoreUnsignedExtensions) { Func<string, bool> isNotSigned = typeUri => !extendableMessage.Extensions.Cast<IOpenIdMessageExtension>().Any(ext => ext.TypeUri == typeUri); foreach (IOpenIdMessageExtension unsignedExtension in this.GetExtensions(extendableMessage, false, isNotSigned)) { + Reporting.RecordFeatureUse(unsignedExtension); unsignedExtension.IsSignedByRemoteParty = false; extendableMessage.Extensions.Add(unsignedExtension); } @@ -219,7 +223,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { yield return extension; } } else { - Logger.OpenId.WarnFormat("Extension with type URI '{0}' ignored because it is not a recognized extension.", typeUri); + Logger.OpenId.DebugFormat("Extension with type URI '{0}' ignored because it is not a recognized extension.", typeUri); } } } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs index 9040404..43d6c03 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs @@ -187,6 +187,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { IReplayProtectedProtocolMessage replayResponse = response; if (!this.nonceStore.StoreNonce(replayResponse.NonceContext, nonce.RandomPartAsString, nonce.CreationDateUtc)) { + Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", replayResponse.Nonce, replayResponse.UtcCreationDate); throw new ReplayedMessageException(message); } @@ -212,7 +213,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { } /// <summary> - /// A special DotNetOpenId-only nonce used by the RP when talking to 1.0 OPs in order + /// A special DotNetOpenAuth-only nonce used by the RP when talking to 1.0 OPs in order /// to protect against replay attacks. /// </summary> private class CustomNonce { diff --git a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs index fd9bb7b..9b166e3 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs @@ -47,12 +47,12 @@ namespace DotNetOpenAuth.OpenId.Extensions { var req = (RelyingParty.AuthenticationRequest)request; var sreg = req.AppliedExtensions.OfType<ClaimsRequest>().SingleOrDefault(); if (sreg == null) { - Logger.OpenId.Warn("No Simple Registration (ClaimsRequest) extension present in the request to spread to AX."); + Logger.OpenId.Debug("No Simple Registration (ClaimsRequest) extension present in the request to spread to AX."); return; } if (req.DiscoveryResult.IsExtensionSupported<ClaimsRequest>()) { - Logger.OpenId.Info("Skipping generation of AX request because the Identifier advertises the Provider supports the Sreg extension."); + Logger.OpenId.Debug("Skipping generation of AX request because the Identifier advertises the Provider supports the Sreg extension."); return; } @@ -65,11 +65,11 @@ namespace DotNetOpenAuth.OpenId.Extensions { // Try to use just one AX Type URI format if we can figure out which type the OP accepts. AXAttributeFormats detectedFormat; if (TryDetectOPAttributeFormat(request, out detectedFormat)) { - Logger.OpenId.Info("Detected OP support for AX but not for Sreg. Removing Sreg extension request and using AX instead."); + Logger.OpenId.Debug("Detected OP support for AX but not for Sreg. Removing Sreg extension request and using AX instead."); attributeFormats = detectedFormat; req.Extensions.Remove(sreg); } else { - Logger.OpenId.Info("Could not determine whether OP supported Sreg or AX. Using both extensions."); + Logger.OpenId.Debug("Could not determine whether OP supported Sreg or AX. Using both extensions."); } foreach (AXAttributeFormats format in ForEachFormat(attributeFormats)) { diff --git a/src/DotNetOpenAuth/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs b/src/DotNetOpenAuth/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs index 0a3147a..3031aad 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs @@ -18,7 +18,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { /// <remarks> /// <para>One using this enum should review the following publication for details /// before asserting or interpreting what these levels signify, notwithstanding - /// the brief summaries attached to each level in DotNetOpenId documentation. + /// the brief summaries attached to each level in DotNetOpenAuth documentation. /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf</para> /// <para> /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels. diff --git a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs index e0317db..edc08ee 100644 --- a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs +++ b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs @@ -142,7 +142,7 @@ namespace DotNetOpenAuth.OpenId { /// Creates a new association of a given type. /// </summary> /// <param name="protocol">The protocol.</param> - /// <param name="associationType">Type of the association.</param> + /// <param name="associationType">Type of the association (i.e. HMAC-SHA1 or HMAC-SHA256)</param> /// <param name="associationUse">A value indicating whether the new association will be used privately by the Provider for "dumb mode" authentication /// or shared with the Relying Party for "smart mode" authentication.</param> /// <param name="securitySettings">The security settings of the Provider.</param> diff --git a/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs b/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs index 86e80ba..fc0f32e 100644 --- a/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs +++ b/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs @@ -100,6 +100,13 @@ namespace DotNetOpenAuth.OpenId.Interop { } /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingPartyShim"/> class. + /// </summary> + public OpenIdRelyingPartyShim() { + Reporting.RecordFeatureUse(this); + } + + /// <summary> /// Creates an authentication request to verify that a user controls /// some given Identifier. /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs index a229488..d22f858 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs @@ -45,6 +45,8 @@ namespace DotNetOpenAuth.OpenId.Provider { // If the openid.claimed_id is present, and if it's different than the openid.identity argument, then // the RP has discovered a claimed identifier that has delegated authentication to this Provider. this.IsDelegatedIdentifier = this.ClaimedIdentifier != null && this.ClaimedIdentifier != this.LocalIdentifier; + + Reporting.RecordEventOccurrence("AuthenticationRequest.IsDelegatedIdentifier", this.IsDelegatedIdentifier.ToString()); } #region HostProcessedRequest members diff --git a/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs index 38d1094..ec0c58a 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs @@ -39,6 +39,7 @@ namespace DotNetOpenAuth.OpenId.Provider { Contract.Requires<ArgumentNullException>(provider != null); this.negativeResponse = new NegativeAssertionResponse(request, provider.Channel); + Reporting.RecordEventOccurrence(this, request.Realm); } #region IHostProcessedRequest Properties diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs index d24873b..611101d 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs @@ -91,6 +91,8 @@ namespace DotNetOpenAuth.OpenId.Provider { } this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings); + + Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore); } /// <summary> @@ -434,6 +436,7 @@ namespace DotNetOpenAuth.OpenId.Provider { } } + Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); return this.Channel.PrepareResponse(positiveAssertion); } @@ -550,6 +553,7 @@ namespace DotNetOpenAuth.OpenId.Provider { private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) { foreach (IProviderBehavior profile in e.NewItems) { profile.ApplySecuritySettings(this.SecuritySettings); + Reporting.RecordFeatureUse(profile); } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index bbb340c..e90c1d3 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -432,7 +432,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { var failedAssociationEndpoints = new List<IdentifierDiscoveryResult>(0); foreach (var endpoint in endpoints) { - Logger.OpenId.InfoFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); // The strategy here is to prefer endpoints with whom we can create associations. Association association = null; @@ -462,10 +462,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (relyingParty.SecuritySettings.RequireAssociation) { Logger.OpenId.Warn("Associations could not be formed with some Providers. Security settings require shared associations for authentication requests so these will be skipped."); } else { - Logger.OpenId.WarnFormat("Now generating requests for Provider endpoints that failed initial association attempts."); + Logger.OpenId.Debug("Now generating requests for Provider endpoints that failed initial association attempts."); foreach (var endpoint in failedAssociationEndpoints) { - Logger.OpenId.WarnFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0} at endpoint: {1}", userSuppliedIdentifier, endpoint.ProviderEndpoint.AbsoluteUri); // Create the auth request, but prevent it from attempting to create an association // because we've already tried. Let's not have it waste time trying again. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs index f899f03..682e3ff 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs @@ -31,6 +31,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires<ArgumentNullException>(exception != null); this.Exception = exception; + + string category = string.Empty; + if (Reporting.Enabled) { + var pe = exception as ProtocolException; + if (pe != null) { + var responseMessage = pe.FaultedMessage as IndirectSignedResponse; + if (responseMessage != null && responseMessage.ProviderEndpoint != null) { // check "required" parts because this is a failure after all + category = responseMessage.ProviderEndpoint.AbsoluteUri; + } + } + + Reporting.RecordEventOccurrence(this, category); + } } #region IAuthenticationResponse Members diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs index 02c5185..869a342 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs @@ -30,6 +30,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal NegativeAuthenticationResponse(NegativeAssertionResponse response) { Contract.Requires<ArgumentNullException>(response != null); this.response = response; + + Reporting.RecordEventOccurrence(this, string.Empty); } #region IAuthenticationResponse Properties diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 4249834..097d065 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -6,7 +6,6 @@ [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")] [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName, "text/css")] -[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedDotNetOpenIdLogoResourceName, "image/gif")] [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName, "image/gif")] [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName, "image/png")] [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName, "image/png")] @@ -46,11 +45,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal const string EmbeddedStylesheetResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdAjaxTextBox.css"; /// <summary> - /// The name of the manifest stream containing the dotnetopenid_16x16.gif file. - /// </summary> - internal const string EmbeddedDotNetOpenIdLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.dotnetopenid_16x16.gif"; - - /// <summary> /// The name of the manifest stream containing the spinner.gif file. /// </summary> internal const string EmbeddedSpinnerResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.spinner.gif"; @@ -704,6 +698,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); + if (!this.Visible) { + return; + } + if (this.DownloadYahooUILibrary) { // Although we'll add the <script> tag to download the YAHOO component, // a download failure may have occurred, so protect ourselves from a @@ -834,9 +832,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { startupScript.AppendFormat("var box = document.getElementsByName('{0}')[0];{1}", this.Name, Environment.NewLine); startupScript.AppendFormat( CultureInfo.InvariantCulture, - "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19}, {20}, function() {{{21};}});{22}", + "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19}, function() {{{20};}});{21}", MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), OpenIdTextBox.EmbeddedLogoResourceName)), - MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedDotNetOpenIdLogoResourceName)), MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedSpinnerResourceName)), MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginSuccessResourceName)), MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginFailureResourceName)), diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js index 65e7ffe..9907b4e 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js @@ -6,7 +6,7 @@ // </copyright> //----------------------------------------------------------------------- -function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url, success_icon_url, failure_icon_url, +function initAjaxOpenId(box, openid_logo_url, spinner_url, success_icon_url, failure_icon_url, throttle, timeout, assertionReceivedCode, loginButtonText, loginButtonToolTip, showLoginPostBackButton, loginPostBackToolTip, retryButtonText, retryButtonToolTip, busyToolTip, @@ -159,8 +159,6 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticatedAsToolTip, true); box.dnoi_internal.failure_icon = box.dnoi_internal.constructIcon(failure_icon_url, authenticationFailedToolTip, true); - // Disable the display of the DotNetOpenId logo - //box.dnoi_internal.dnoi_logo = box.dnoi_internal.constructIcon(dotnetopenid_logo_url); box.dnoi_internal.dnoi_logo = box.dnoi_internal.openid_logo; box.dnoi_internal.setVisualCue = function(state, authenticatedBy, authenticatedAs, providers, errorMessage) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs index ce77df1..dbf9530 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs @@ -244,6 +244,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Initializes a new instance of the <see cref="OpenIdMobileTextBox"/> class. /// </summary> public OpenIdMobileTextBox() { + Reporting.RecordFeatureUse(this); } #region Events diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index d14e3e3..d199985 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -114,6 +114,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings); this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings); + + Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore); } /// <summary> @@ -646,6 +648,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) { foreach (IRelyingPartyBehavior profile in e.NewItems) { profile.ApplySecuritySettings(this.SecuritySettings); + Reporting.RecordFeatureUse(profile); } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 8a33f3b..12d676b 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -174,6 +174,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { }; this.authenticationResponse = this.RelyingParty.GetResponse(clientResponseInfo); + Logger.Controls.DebugFormat( + "The {0} control checked for an authentication response and found: {1}", + this.ID, + this.authenticationResponse.Status); this.AuthenticationProcessedAlready = false; // Save out the authentication response to viewstate so we can find it on @@ -416,10 +420,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// </summary> protected override void ScriptClosingPopupOrIFrame() { - Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); + Logger.OpenId.DebugFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); string extensionsJson = null; var authResponse = RelyingPartyNonVerifying.GetResponse(); + Logger.Controls.DebugFormat( + "The {0} control checked for an authentication response from a popup window or iframe using a non-verifying RP and found: {1}", + this.ID, + authResponse.Status); if (authResponse.Status == AuthenticationStatus.Authenticated) { this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. var extensionsDictionary = new Dictionary<string, string>(); @@ -613,7 +621,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> private void CallbackUserAgentMethod(string methodCall) { - Logger.OpenId.InfoFormat("Sending Javascript callback: {0}", methodCall); + Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); Page.Response.Write(@"<html><body><script language='javascript'> var inPopup = !window.frameElement; var objSrc = inPopup ? window.opener : window.frameElement; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index 1fe53d8..cb50fa6 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -244,6 +244,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class. /// </summary> protected OpenIdRelyingPartyControlBase() { + Reporting.RecordFeatureUse(this); } #region Events @@ -481,6 +482,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Gets a value indicating whether this control is a child control of a composite OpenID control. + /// </summary> + /// <value> + /// <c>true</c> if this instance is embedded in parent OpenID control; otherwise, <c>false</c>. + /// </value> + protected bool IsEmbeddedInParentOpenIdControl { + get { return this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().Any(); } + } + + /// <summary> /// Clears any cookie set by this control to help the user on a returning visit next time. /// </summary> public static void LogOff() { @@ -607,11 +618,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return; // don't do any more processing on it now } - // Only sniff for an OpenID response if it is targeted at this control. Note that - // Stateless mode causes no receiver to be indicated. + // Only sniff for an OpenID response if it is targeted at this control. + // Note that Stateless mode causes no receiver to be indicated, and + // we want to handle that, but only if there isn't a parent control that + // will be handling that. string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId]; - if (receiver == null || receiver == this.ClientID) { + if (receiver == this.ClientID || (receiver == null && !this.IsEmbeddedInParentOpenIdControl)) { var response = this.RelyingParty.GetResponse(); + Logger.Controls.DebugFormat( + "The {0} control checked for an authentication response and found: {1}", + this.ID, + response != null ? response.Status.ToString() : "nothing"); this.ProcessResponse(response); } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs index ed83412..e93383d 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs @@ -81,6 +81,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private HiddenField positiveAssertionField; /// <summary> + /// A field to store the value to set on the <see cref="textBox"/> control after it's created. + /// </summary> + private bool downloadYuiLibrary = OpenIdAjaxTextBox.DownloadYahooUILibraryDefault; + + /// <summary> /// Initializes a new instance of the <see cref="OpenIdSelector"/> class. /// </summary> public OpenIdSelector() { @@ -121,13 +126,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Description("Whether a split button will be used for the \"log in\" when the user provides an identifier that delegates to more than one Provider.")] public bool DownloadYahooUILibrary { get { - this.EnsureChildControls(); - return this.textBox.DownloadYahooUILibrary; + return this.textBox != null ? this.textBox.DownloadYahooUILibrary : this.downloadYuiLibrary; } set { - this.EnsureChildControls(); - this.textBox.DownloadYahooUILibrary = value; + // We don't just call EnsureChildControls() and then set the property on + // this.textBox itself because (apparently) setting this property in the ASPX + // page and thus calling this EnsureID() via EnsureChildControls() this early + // results in no ID. + if (this.textBox != null) { + this.textBox.DownloadYahooUILibrary = value; + } else { + this.downloadYuiLibrary = value; + } } } @@ -171,6 +182,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Gets a value indicating whether some button in the selector will want + /// to display the <see cref="OpenIdAjaxTextBox"/> control. + /// </summary> + protected virtual bool OpenIdTextBoxVisible { + get { return this.Buttons.OfType<SelectorOpenIdButton>().Any(); } + } + + /// <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> @@ -190,6 +209,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected override void CreateChildControls() { base.CreateChildControls(); this.EnsureID(); + ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(this.UniqueID), "Control.EnsureID() failed to give us a unique ID. Try setting an ID on the OpenIdSelector control. But please also file this bug with the project owners."); var selectorButton = this.Buttons.OfType<SelectorInfoCardButton>().FirstOrDefault(); if (selectorButton != null) { @@ -205,6 +225,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.textBox.ID = "openid_identifier"; this.textBox.HookFormSubmit = false; this.textBox.ShowLogOnPostBackButton = true; + this.textBox.DownloadYahooUILibrary = this.downloadYuiLibrary; this.Controls.Add(this.textBox); this.positiveAssertionField = new HiddenField(); @@ -259,6 +280,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "Postback", script, true); this.PreloadDiscovery(this.Buttons.OfType<SelectorProviderButton>().Select(op => op.OPIdentifier).Where(id => id != null)); + this.textBox.Visible = this.OpenIdTextBoxVisible; } /// <summary> @@ -295,13 +317,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { writer.RenderEndTag(); // </ul> - writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none"); - writer.AddAttribute(HtmlTextWriterAttribute.Id, "OpenIDForm"); - writer.RenderBeginTag(HtmlTextWriterTag.Div); + if (this.textBox.Visible) { + writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none"); + writer.AddAttribute(HtmlTextWriterAttribute.Id, "OpenIDForm"); + writer.RenderBeginTag(HtmlTextWriterTag.Div); - this.textBox.RenderControl(writer); + this.textBox.RenderControl(writer); - writer.RenderEndTag(); // </div> + writer.RenderEndTag(); // </div> + } this.positiveAssertionField.RenderControl(writer); } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js index 6271952..c58e06e 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js @@ -10,7 +10,7 @@ $(function() { var hint = $.cookie('openid_identifier') || ''; var ajaxbox = document.getElementsByName('openid_identifier')[0]; - if (hint != 'infocard') { + if (ajaxbox && hint != 'infocard') { ajaxbox.setValue(hint); } @@ -31,12 +31,18 @@ $(function() { } }); if (!matchFound) { - $('#OpenIDButton') - .removeClass('grayedOut') - .addClass('focused'); - $('#OpenIDForm').show('slow', function() { - ajaxbox.focus(); - }); + if (ajaxbox) { + $('#OpenIDButton') + .removeClass('grayedOut') + .addClass('focused'); + $('#OpenIDForm').show('slow', function() { + ajaxbox.focus(); + }); + } else { + // No OP button matched the last identifier, and there is no text box, + // so just un-gray all buttons. + ops.removeClass('grayedOut'); + } } } @@ -65,13 +71,15 @@ $(function() { } }); - ajaxbox.onStateChanged = function(state) { - if (state == "authenticated") { - showLoginSuccess('OpenIDButton', true); - } else { - showLoginSuccess('OpenIDButton', false); // hide checkmark - } - }; + if (ajaxbox) { + ajaxbox.onStateChanged = function(state) { + if (state == "authenticated") { + showLoginSuccess('OpenIDButton', true); + } else { + showLoginSuccess('OpenIDButton', false); // hide checkmark + } + }; + } function checkidSetup(identifier, timerBased) { var openid = new window.OpenIdIdentifier(identifier); @@ -88,8 +96,10 @@ $(function() { window.postLoginAssertion(respondingEndpoint.response.toString(), window.parent.location.href); } - // take over how the text box does postbacks. - ajaxbox.dnoi_internal.postback = doLogin; + if (ajaxbox) { + // take over how the text box does postbacks. + ajaxbox.dnoi_internal.postback = doLogin; + } // This FrameManager will be used for background logins for the OP buttons // and the last used identifier. It is NOT the frame manager used by the @@ -138,7 +148,7 @@ $(function() { // Don't immediately login if the user clicked OpenID and he can't see the identifier box. if ($(this)[0].id != 'OpenIDButton') { relevantUserSuppliedIdentifier = $(this)[0].id; - } else if ($('#OpenIDForm').is(':visible')) { + } else if (ajaxbox && $('#OpenIDForm').is(':visible')) { relevantUserSuppliedIdentifier = ajaxbox.value; } @@ -157,16 +167,18 @@ $(function() { $('img', this)[0].click(); } }); - $('#OpenIDButton').click(function() { - // Be careful to only try to select the text box once it is available. - if ($('#OpenIDForm').is(':hidden')) { - $('#OpenIDForm').show('slow', function() { + if (ajaxbox) { + $('#OpenIDButton').click(function() { + // Be careful to only try to select the text box once it is available. + if ($('#OpenIDForm').is(':hidden')) { + $('#OpenIDForm').show('slow', function() { + ajaxbox.focus(); + }); + } else { ajaxbox.focus(); - }); - } else { - ajaxbox.focus(); - } - }); + } + }); + } // Make popup window close on escape (the dialog style is already taken care of) $(document).keydown(function(e) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs index 8a62d68..5cfa191 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs @@ -39,6 +39,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (response.ProviderEndpoint != null && response.Version != null) { this.provider = new ProviderEndpointDescription(response.ProviderEndpoint, response.Version); } + + // Derived types of this are responsible to log an appropriate message for themselves. + if (Logger.OpenId.IsInfoEnabled && this.GetType() == typeof(PositiveAnonymousResponse)) { + Logger.OpenId.Info("Received anonymous (identity-less) positive assertion."); + } + + if (response.ProviderEndpoint != null) { + Reporting.RecordEventOccurrence(this, response.ProviderEndpoint.AbsoluteUri); + } } #region IAuthenticationResponse Properties @@ -128,6 +137,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> + /// Gets a value indicating whether trusted callback arguments are available. + /// </summary> + /// <remarks> + /// We use this internally to avoid logging a warning during a standard snapshot creation. + /// </remarks> + internal bool TrustedCallbackArgumentsAvailable { + get { return this.response.ReturnToParametersSignatureValidated; } + } + + /// <summary> /// Gets the positive extension-only message the Relying Party received that this instance wraps. /// </summary> protected internal IndirectSignedResponse Response { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index 44f01bc..6e141ad 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -37,6 +37,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { null); this.VerifyDiscoveryMatchesAssertion(relyingParty); + + Logger.OpenId.InfoFormat("Received identity assertion for {0} via {1}.", this.ClaimedIdentifier, this.Provider.Uri); } #region IAuthenticationResponse Properties diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs index 32c8af9..80b424a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs @@ -39,8 +39,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.FriendlyIdentifierForDisplay = copyFrom.FriendlyIdentifierForDisplay; this.Status = copyFrom.Status; this.Provider = copyFrom.Provider; - this.callbackArguments = copyFrom.GetCallbackArguments(); this.untrustedCallbackArguments = copyFrom.GetUntrustedCallbackArguments(); + + // Do this special check to avoid logging a warning for trying to clone a dictionary. + var anonResponse = copyFrom as PositiveAnonymousResponse; + if (anonResponse == null || anonResponse.TrustedCallbackArgumentsAvailable) { + this.callbackArguments = copyFrom.GetCallbackArguments(); + } else { + this.callbackArguments = EmptyDictionary<string, string>.Instance; + } } #region IAuthenticationResponse Members diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs index 74e37a6..c5dda1c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs @@ -26,6 +26,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Initializes a new instance of the <see cref="SelectorInfoCardButton"/> class. /// </summary> public SelectorInfoCardButton() { + Reporting.RecordFeatureUse(this); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs index d20bc2b..15b6ca7 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs @@ -20,6 +20,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class. /// </summary> public SelectorOpenIdButton() { + Reporting.RecordFeatureUse(this); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs index d6d1339..3a05287 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs @@ -21,6 +21,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Initializes a new instance of the <see cref="SelectorProviderButton"/> class. /// </summary> public SelectorProviderButton() { + Reporting.RecordFeatureUse(this); } /// <summary> diff --git a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs index 51d146c..0bba853 100644 --- a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs @@ -83,13 +83,15 @@ using System.Web.UI; // match the one used by hosting providers. Listing them individually seems to be more common. [assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"http://.*")] [assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"https://.*")] - #if PARTIAL_TRUST // Allows hosting this assembly in an ASP.NET setting. Not all applications // will host this using ASP.NET, so this is optional. Besides, we need at least // one optional permission to activate CAS permission shrinking. [assembly: AspNetHostingPermission(SecurityAction.RequestOptional, Level = AspNetHostingPermissionLevel.Medium)] +// Allows this assembly to store reporting data. +[assembly: IsolatedStorageFilePermission(SecurityAction.RequestOptional, UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByUser)] + // The following are only required for diagnostic logging (Trace.Write, Debug.Assert, etc.). #if TRACE || DEBUG [assembly: KeyContainerPermission(SecurityAction.RequestOptional, Unrestricted = true)] diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs new file mode 100644 index 0000000..4e4bbf5 --- /dev/null +++ b/src/DotNetOpenAuth/Reporting.cs @@ -0,0 +1,841 @@ +//----------------------------------------------------------------------- +// <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.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; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// The statistical reporting mechanism used so this library's project authors + /// know what versions and features are in use. + /// </summary> + internal static class Reporting { + /// <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> + static Reporting() { + Enabled = DotNetOpenAuthSection.Configuration.Reporting.Enabled; + if (Enabled) { + 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"); + } + } catch (Exception e) { + // This is supposed to be as low-risk as possible, so if it fails, just disable reporting + // and avoid rethrowing. + Enabled = false; + Logger.Library.Error("Error while trying to initialize reporting.", e); + } + } + } + + /// <summary> + /// Gets or sets a value indicating whether this reporting is enabled. + /// </summary> + /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> + internal static bool Enabled { get; set; } + + /// <summary> + /// Gets the configuration to use for reporting. + /// </summary> + private static ReportingElement Configuration { + get { return DotNetOpenAuthSection.Configuration.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> + 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> + /// <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 the feature and dependency use. + /// </summary> + /// <param name="value">The consumer or service provider.</param> + /// <param name="service">The service.</param> + /// <param name="tokenManager">The token manager.</param> + /// <param name="nonceStore">The nonce store.</param> + internal static void RecordFeatureAndDependencyUse(object value, ServiceProviderDescription service, ITokenManager tokenManager, INonceStore nonceStore) { + Contract.Requires(value != null); + Contract.Requires(service != null); + Contract.Requires(tokenManager != null); + + // In release builds, just quietly return. + if (value == null || service == null || tokenManager == null) { + return; + } + + if (Enabled && Configuration.IncludeFeatureUsage) { + StringBuilder builder = new StringBuilder(); + builder.Append(value.GetType().Name); + builder.Append(" "); + builder.Append(tokenManager.GetType().Name); + if (nonceStore != null) { + builder.Append(" "); + builder.Append(nonceStore.GetType().Name); + } + builder.Append(" "); + builder.Append(service.Version); + builder.Append(" "); + builder.Append(service.UserAuthorizationEndpoint); + 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> + /// Assembles a report for submission. + /// </summary> + /// <returns>A stream that contains the report.</returns> + private static Stream GetReport() { + var stream = new MemoryStream(); + 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; + } + + /// <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> + /// Called by every internal/public method on this class to give + /// periodic operations a chance to run. + /// </summary> + private 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> + /// Sends the stats report asynchronously, and careful to not throw any unhandled exceptions. + /// </summary> + 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); + Enabled = false; + } + }); + } + + /// <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() { + Contract.Requires<InvalidOperationException>(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) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(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> + private 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) { + Contract.Requires<ArgumentNullException>(storage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(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, Encoding.UTF8); + 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); + } + + #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) { + Contract.Requires<ArgumentNullException>(storage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(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, Encoding.UTF8); + 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); + } + + #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/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs index 332e3fa..e7c04d8 100644 --- a/src/DotNetOpenAuth/XrdsPublisher.cs +++ b/src/DotNetOpenAuth/XrdsPublisher.cs @@ -95,6 +95,13 @@ namespace DotNetOpenAuth { /// </summary> private const string EnabledViewStateKey = "Enabled"; + /// <summary> + /// Initializes a new instance of the <see cref="XrdsPublisher"/> class. + /// </summary> + public XrdsPublisher() { + Reporting.RecordFeatureUse(this); + } + #region Properties /// <summary> diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index a9a573b..589a155 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -73,6 +73,7 @@ namespace DotNetOpenAuth.Yadis { CachedDirectWebResponse response2 = null; if (IsXrdsDocument(response)) { Logger.Yadis.Debug("An XRDS response was received from GET at user-supplied identifier."); + Reporting.RecordEventOccurrence("Yadis", "XRDS in initial response"); response2 = response; } else { string uriString = response.Headers.Get(HeaderName); @@ -80,12 +81,14 @@ namespace DotNetOpenAuth.Yadis { if (uriString != null) { if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) { Logger.Yadis.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url); + Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTTP header"); } } if (url == null && response.ContentType != null && (response.ContentType.MediaType == ContentTypes.Html || response.ContentType.MediaType == ContentTypes.XHtml)) { url = FindYadisDocumentLocationInHtmlMetaTags(response.GetResponseString()); if (url != null) { Logger.Yadis.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url); + Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTML"); } } if (url != null) { |