summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2009-12-27 07:05:51 -0800
committerAndrew Arnott <andrewarnott@gmail.com>2009-12-27 07:05:51 -0800
commitcf48d319968262e3c7f7322ea17fedcd3819d743 (patch)
treedcc71264953aaf07b62aae065c4aff6d10a83a16 /src
parenta3538a47a43eb00461f68fb542097871f732429a (diff)
parent505a4ae9f092e8092cc7d3b142b05614fda318f2 (diff)
downloadDotNetOpenAuth-cf48d319968262e3c7f7322ea17fedcd3819d743.zip
DotNetOpenAuth-cf48d319968262e3c7f7322ea17fedcd3819d743.tar.gz
DotNetOpenAuth-cf48d319968262e3c7f7322ea17fedcd3819d743.tar.bz2
Merge branch 'v3.3'
Conflicts: samples/InfoCardRelyingParty/Site.Master samples/OpenIdProviderWebForms/Site.Master samples/OpenIdRelyingPartyClassicAsp/MembersOnly.asp samples/OpenIdRelyingPartyClassicAsp/default.asp samples/OpenIdRelyingPartyClassicAsp/login.asp samples/OpenIdRelyingPartyWebForms/Site.Master src/DotNetOpenAuth.vsmdi src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs
Diffstat (limited to 'src')
-rw-r--r--src/DotNetOpenAuth.BuildTasks/CompareFiles.cs27
-rw-r--r--src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs14
-rw-r--r--src/DotNetOpenAuth.Test/App.config2
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd577
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs14
-rw-r--r--src/DotNetOpenAuth/Configuration/OpenIdElement.cs4
-rw-r--r--src/DotNetOpenAuth/Configuration/ReportingElement.cs139
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj2
-rw-r--r--src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs1
-rw-r--r--src/DotNetOpenAuth/Logger.cs10
-rw-r--r--src/DotNetOpenAuth/Loggers/ILog.cs2
-rw-r--r--src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs1
-rw-r--r--src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs10
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs1
-rw-r--r--src/DotNetOpenAuth/OAuth/ConsumerBase.cs2
-rw-r--r--src/DotNetOpenAuth/OAuth/ServiceProvider.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs6
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs3
-rw-r--r--src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs8
-rw-r--r--src/DotNetOpenAuth/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs7
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs6
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js4
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs3
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs12
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs23
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs42
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js64
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs19
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs9
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs1
-rw-r--r--src/DotNetOpenAuth/Properties/AssemblyInfo.cs4
-rw-r--r--src/DotNetOpenAuth/Reporting.cs841
-rw-r--r--src/DotNetOpenAuth/XrdsPublisher.cs7
-rw-r--r--src/DotNetOpenAuth/Yadis/Yadis.cs3
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 &lt;reporting&gt; 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 &lt;reporting&gt; 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) {