diff options
39 files changed, 1118 insertions, 37 deletions
diff --git a/projecttemplates/WebFormsRelyingParty/Web.config b/projecttemplates/WebFormsRelyingParty/Web.config index f0b01ff..1d7c29f 100644 --- a/projecttemplates/WebFormsRelyingParty/Web.config +++ b/projecttemplates/WebFormsRelyingParty/Web.config @@ -1,4 +1,12 @@ <?xml version="1.0"?> +<!-- + Note: As an alternative to hand editing this file you can use the + web admin tool to configure settings for your application. Use + the Website->Asp.Net Configuration option in Visual Studio. + A full list of settings and comments can be found in + machine.config.comments usually located in + \Windows\Microsoft.Net\Framework\v2.x\Config +--> <configuration> <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> @@ -16,6 +24,7 @@ </sectionGroup> </sectionGroup> </configSections> + <!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names), which is necessary for OpenID urls with unicode characters in the domain/host name. It is also required to put the Uri class into RFC 3986 escaping mode, which OpenID and OAuth require. --> @@ -23,6 +32,7 @@ <idn enabled="All" /> <iriParsing enabled="true" /> </uri> + <system.net> <defaultProxy enabled="true" /> <settings> @@ -32,6 +42,7 @@ <servicePointManager checkCertificateRevocationList="true" /> </settings> </system.net> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> <dotNetOpenAuth> <messaging> @@ -56,7 +67,10 @@ <store type="RelyingPartyLogic.NonceDbStore, RelyingPartyLogic"/> </serviceProvider> </oauth> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> </dotNetOpenAuth> + <!-- log4net is a 3rd party (free) logger library that DotNetOpenAuth will use if present but does not require. --> <log4net> <appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender"> @@ -249,4 +263,4 @@ </authorization> </system.web> </location> -</configuration>
\ No newline at end of file +</configuration> diff --git a/samples/InfoCardRelyingParty/web.config b/samples/InfoCardRelyingParty/web.config index f14d14b..d91f9e8 100644 --- a/samples/InfoCardRelyingParty/web.config +++ b/samples/InfoCardRelyingParty/web.config @@ -1,15 +1,8 @@ <?xml version="1.0"?> -<!-- - Note: As an alternative to hand editing this file you can use the - web admin tool to configure settings for your application. Use - the Website->Asp.Net Configuration option in Visual Studio. - A full list of settings and comments can be found in - machine.config.comments usually located in - \Windows\Microsoft.Net\Framework\v2.x\Config ---> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" /> + <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/> <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/> @@ -23,6 +16,12 @@ </sectionGroup> </configSections> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> + <dotNetOpenAuth> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> + </dotNetOpenAuth> + <log4net> <appender name="TracePageAppender" type="TracePageAppender, __code"> <layout type="log4net.Layout.PatternLayout"> @@ -41,13 +40,13 @@ </logger> </log4net> - <appSettings/> - <connectionStrings/> - <system.net> <defaultProxy enabled="true" /> </system.net> + <appSettings/> + <connectionStrings/> + <system.web> <!-- Set compilation debug="true" to insert debugging diff --git a/samples/OAuthConsumer/Web.config b/samples/OAuthConsumer/Web.config index c3a808a..9467175 100644 --- a/samples/OAuthConsumer/Web.config +++ b/samples/OAuthConsumer/Web.config @@ -3,6 +3,7 @@ <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" /> + <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/> <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/> @@ -34,6 +35,12 @@ </settings> </system.net> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> + <dotNetOpenAuth> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> + </dotNetOpenAuth> + <appSettings> <!-- Fill in your various consumer keys and secrets here to make the sample work. --> <!-- You must get these values by signing up with each individual service provider. --> diff --git a/samples/OAuthConsumerWpf/App.config b/samples/OAuthConsumerWpf/App.config index e53b4a3..f142405 100644 --- a/samples/OAuthConsumerWpf/App.config +++ b/samples/OAuthConsumerWpf/App.config @@ -3,6 +3,7 @@ <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" /> + <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/> </configSections> <!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names), @@ -23,6 +24,12 @@ </settings> </system.net> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> + <dotNetOpenAuth> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true"/> + </dotNetOpenAuth> + <appSettings> <!-- Fill in your various consumer keys and secrets here to make the sample work. --> <!-- You must get these values by signing up with each individual service provider. --> diff --git a/samples/OAuthServiceProvider/Web.config b/samples/OAuthServiceProvider/Web.config index d039daa..c21ebd4 100644 --- a/samples/OAuthServiceProvider/Web.config +++ b/samples/OAuthServiceProvider/Web.config @@ -3,6 +3,7 @@ <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false"/> + <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/> <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"> <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/> @@ -34,6 +35,12 @@ </settings> </system.net> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> + <dotNetOpenAuth> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> + </dotNetOpenAuth> + <appSettings/> <connectionStrings> <add name="DatabaseConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database.mdf;Integrated Security=True;User Instance=True" diff --git a/samples/OpenIdOfflineProvider/App.config b/samples/OpenIdOfflineProvider/App.config index cd04b13..7263338 100644 --- a/samples/OpenIdOfflineProvider/App.config +++ b/samples/OpenIdOfflineProvider/App.config @@ -1,9 +1,20 @@ <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> + <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth" requirePermission="false" allowLocation="true"/> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" requirePermission="false"/> </configSections> + + <!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names), + which is necessary for OpenID urls with unicode characters in the domain/host name. + It is also required to put the Uri class into RFC 3986 escaping mode, which OpenID and OAuth require. --> + <uri> + <idn enabled="All"/> + <iriParsing enabled="true"/> + </uri> + + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> <dotNetOpenAuth> <messaging> <untrustedWebRequest> @@ -13,6 +24,8 @@ </whitelistHosts> </untrustedWebRequest> </messaging> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true"/> </dotNetOpenAuth> <log4net> <appender name="TextBoxAppender" type="log4net.Appender.TextWriterAppender"> diff --git a/samples/OpenIdProviderMvc/Web.config b/samples/OpenIdProviderMvc/Web.config index f36bfcf..93dbf58 100644 --- a/samples/OpenIdProviderMvc/Web.config +++ b/samples/OpenIdProviderMvc/Web.config @@ -1,12 +1,4 @@ <?xml version="1.0"?> -<!-- - Note: As an alternative to hand editing this file you can use the - web admin tool to configure settings for your application. Use - the Website->Asp.Net Configuration option in Visual Studio. - A full list of settings and comments can be found in - machine.config.comments usually located in - \Windows\Microsoft.Net\Framework\v2.x\Config ---> <configuration> <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> @@ -67,6 +59,8 @@ </whitelistHosts> </untrustedWebRequest> </messaging> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> </dotNetOpenAuth> <appSettings/> diff --git a/samples/OpenIdProviderWebForms/Web.config b/samples/OpenIdProviderWebForms/Web.config index 159dcd1..a662be3 100644 --- a/samples/OpenIdProviderWebForms/Web.config +++ b/samples/OpenIdProviderWebForms/Web.config @@ -1,12 +1,4 @@ <?xml version="1.0"?> -<!-- - Note: As an alternative to hand editing this file you can use the - web admin tool to configure settings for your application. Use - the Website->Asp.Net Configuration option in Visual Studio. - A full list of settings and comments can be found in - machine.config.comments usually located in - \Windows\Microsoft.Net\Framework\v2.x\Config ---> <configuration> <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> @@ -66,6 +58,8 @@ </whitelistHosts> </untrustedWebRequest> </messaging> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> </dotNetOpenAuth> <system.web> diff --git a/samples/OpenIdRelyingPartyMvc/Web.config b/samples/OpenIdRelyingPartyMvc/Web.config index c3bfa41..315eaa9 100644 --- a/samples/OpenIdRelyingPartyMvc/Web.config +++ b/samples/OpenIdRelyingPartyMvc/Web.config @@ -1,12 +1,4 @@ <?xml version="1.0"?> -<!-- - Note: As an alternative to hand editing this file you can use the - web admin tool to configure settings for your application. Use - the Website->Asp.Net Configuration option in Visual Studio. - A full list of settings and comments can be found in - machine.config.comments usually located in - \Windows\Microsoft.Net\Framework\v2.x\Config ---> <configuration> <configSections> <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> @@ -62,6 +54,8 @@ </whitelistHosts> </untrustedWebRequest> </messaging> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> </dotNetOpenAuth> <appSettings/> diff --git a/samples/OpenIdRelyingPartyWebForms/Web.config b/samples/OpenIdRelyingPartyWebForms/Web.config index c48c198..0eea738 100644 --- a/samples/OpenIdRelyingPartyWebForms/Web.config +++ b/samples/OpenIdRelyingPartyWebForms/Web.config @@ -23,6 +23,7 @@ <!--<servicePointManager checkCertificateRevocationList="true"/>--> </settings> </system.net> + <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized --> <dotNetOpenAuth> <openid> @@ -46,6 +47,8 @@ </whitelistHosts> </untrustedWebRequest> </messaging> + <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> + <reporting enabled="true" /> </dotNetOpenAuth> <appSettings> diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index a637d1f..63ad100 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -313,6 +313,17 @@ </xs:choice> </xs:complexType> </xs:element> + <xs:element name="reporting"> + <xs:complexType> + <xs:attribute name="enabled" type="xs:boolean" /> + <xs:attribute name="minimumReportingInterval" type="xs:string" /> + <xs:attribute name="minimumFlushInterval" type="xs:string" /> + <xs:attribute name="includeFeatureUsage" type="xs:boolean" default="true" /> + <xs:attribute name="includeEventStatistics" type="xs:boolean" default="true" /> + <xs:attribute name="includeLocalRequestUris" type="xs:boolean" default="true" /> + <xs:attribute name="includeCultures" type="xs:boolean" default="true" /> + </xs:complexType> + </xs:element> </xs:choice> </xs:complexType> </xs:element> diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs index 7bd84d9..aa956d1 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs @@ -35,6 +35,11 @@ namespace DotNetOpenAuth.Configuration { private const string OAuthElementName = "oauth"; /// <summary> + /// The name of the <reporting> sub-element. + /// </summary> + private const string ReportingElementName = "reporting"; + + /// <summary> /// Initializes a new instance of the <see cref="DotNetOpenAuthSection"/> class. /// </summary> internal DotNetOpenAuthSection() { @@ -75,5 +80,14 @@ namespace DotNetOpenAuth.Configuration { get { return (OAuthElement)this[OAuthElementName] ?? new OAuthElement(); } set { this[OAuthElementName] = value; } } + + /// <summary> + /// Gets or sets the configuration for reporting. + /// </summary> + [ConfigurationProperty(ReportingElementName)] + internal ReportingElement Reporting { + get { return (ReportingElement)this[ReportingElementName] ?? new ReportingElement(); } + set { this[ReportingElementName] = value; } + } } } diff --git a/src/DotNetOpenAuth/Configuration/ReportingElement.cs b/src/DotNetOpenAuth/Configuration/ReportingElement.cs new file mode 100644 index 0000000..ab1437f --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/ReportingElement.cs @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------- +// <copyright file="ReportingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Linq; + using System.Text; + + /// <summary> + /// Represents the <reporting> element in the host's .config file. + /// </summary> + internal class ReportingElement : ConfigurationElement { + /// <summary> + /// The name of the @enabled attribute. + /// </summary> + private const string EnabledAttributeName = "enabled"; + + /// <summary> + /// The name of the @minimumReportingInterval attribute. + /// </summary> + private const string MinimumReportingIntervalAttributeName = "minimumReportingInterval"; + + /// <summary> + /// The name of the @minimumFlushInterval attribute. + /// </summary> + private const string MinimumFlushIntervalAttributeName = "minimumFlushInterval"; + + /// <summary> + /// The name of the @includeFeatureUsage attribute. + /// </summary> + private const string IncludeFeatureUsageAttributeName = "includeFeatureUsage"; + + /// <summary> + /// The name of the @includeEventStatistics attribute. + /// </summary> + private const string IncludeEventStatisticsAttributeName = "includeEventStatistics"; + + /// <summary> + /// The name of the @includeLocalRequestUris attribute. + /// </summary> + private const string IncludeLocalRequestUrisAttributeName = "includeLocalRequestUris"; + + /// <summary> + /// The name of the @includeCultures attribute. + /// </summary> + private const string IncludeCulturesAttributeName = "includeCultures"; + + /// <summary> + /// The default value for the @minimumFlushInterval attribute. + /// </summary> +#if DEBUG + private const string MinimumFlushIntervalDefault = "0"; +#else + private const string MinimumFlushIntervalDefault = "0:15"; +#endif + + /// <summary> + /// Initializes a new instance of the <see cref="ReportingElement"/> class. + /// </summary> + internal ReportingElement() { + } + + /// <summary> + /// Gets or sets a value indicating whether this reporting is enabled. + /// </summary> + /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> + [ConfigurationProperty(EnabledAttributeName, DefaultValue = false)] + internal bool Enabled { + get { return (bool)this[EnabledAttributeName]; } + set { this[EnabledAttributeName] = value; } + } + + /// <summary> + /// Gets or sets the maximum frequency that reports will be published. + /// </summary> + [ConfigurationProperty(MinimumReportingIntervalAttributeName, DefaultValue = "1")] // 1 day default + internal TimeSpan MinimumReportingInterval { + get { return (TimeSpan)this[MinimumReportingIntervalAttributeName]; } + set { this[MinimumReportingIntervalAttributeName] = value; } + } + + /// <summary> + /// Gets or sets the maximum frequency the set can be flushed to disk. + /// </summary> + [ConfigurationProperty(MinimumFlushIntervalAttributeName, DefaultValue = MinimumFlushIntervalDefault)] + internal TimeSpan MinimumFlushInterval { + get { return (TimeSpan)this[MinimumFlushIntervalAttributeName]; } + set { this[MinimumFlushIntervalAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include a list of library features used in the report. + /// </summary> + /// <value><c>true</c> to include a report of features used; otherwise, <c>false</c>.</value> + [ConfigurationProperty(IncludeFeatureUsageAttributeName, DefaultValue = true)] + internal bool IncludeFeatureUsage { + get { return (bool)this[IncludeFeatureUsageAttributeName]; } + set { this[IncludeFeatureUsageAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include statistics of certain events such as + /// authentication success and failure counting, and can include remote endpoint URIs. + /// </summary> + /// <value> + /// <c>true</c> to include event counters in the report; otherwise, <c>false</c>. + /// </value> + [ConfigurationProperty(IncludeEventStatisticsAttributeName, DefaultValue = true)] + internal bool IncludeEventStatistics { + get { return (bool)this[IncludeEventStatisticsAttributeName]; } + set { this[IncludeEventStatisticsAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include a few URLs to pages on the hosting + /// web site that host DotNetOpenAuth components. + /// </summary> + [ConfigurationProperty(IncludeLocalRequestUrisAttributeName, DefaultValue = true)] + internal bool IncludeLocalRequestUris { + get { return (bool)this[IncludeLocalRequestUrisAttributeName]; } + set { this[IncludeLocalRequestUrisAttributeName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to include the cultures requested by the user agent + /// on pages that host DotNetOpenAuth components. + /// </summary> + [ConfigurationProperty(IncludeCulturesAttributeName, DefaultValue = true)] + internal bool IncludeCultures { + get { return (bool)this[IncludeCulturesAttributeName]; } + set { this[IncludeCulturesAttributeName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 42b99fd..4b73c05 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -229,6 +229,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" /> @@ -556,6 +557,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/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 3f07e5a..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); } 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 3eb24d4..827ca6c 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs @@ -84,6 +84,8 @@ namespace DotNetOpenAuth.OpenId.Provider { } this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings); + + Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore); } /// <summary> @@ -401,6 +403,7 @@ namespace DotNetOpenAuth.OpenId.Provider { } } + Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); return this.Channel.PrepareResponse(positiveAssertion); } @@ -513,6 +516,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/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/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 e2d6356..55d6403 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -104,6 +104,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> @@ -593,6 +595,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/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index d182684..2420fd6 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 diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs index 1c6655d..7a1fbbf 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs @@ -44,6 +44,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (Logger.OpenId.IsInfoEnabled && this.GetType() == typeof(PositiveAnonymousResponse)) { Logger.OpenId.Info("Received anonymous (identity-less) positive assertion."); } + + Reporting.RecordEventOccurrence(this, response.ProviderEndpoint.AbsoluteUri); } #region IAuthenticationResponse Properties 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..15af4ba --- /dev/null +++ b/src/DotNetOpenAuth/Reporting.cs @@ -0,0 +1,806 @@ +//----------------------------------------------------------------------- +// <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<ArgumentException>(!String.IsNullOrEmpty(eventName)); + + 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<ArgumentNullException>(eventNameByObjectType != null); + + 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<ArgumentException>(!String.IsNullOrEmpty(feature)); + + 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<ArgumentNullException>(value != null); + + 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<ArgumentNullException>(value != null); + + 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<ArgumentNullException>(value != null); + Contract.Requires<ArgumentNullException>(service != null); + Contract.Requires<ArgumentNullException>(tokenManager != null); + + 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<ArgumentNullException>(request != null); + + 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) { |