//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System.Collections.Generic; using System.Diagnostics; using System.Runtime; using System.Runtime.Serialization; using System.Security; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Diagnostics; using System.ServiceModel.Dispatcher; using System.Web; using System.Web.Caching; using System.Web.Configuration; using System.Web.UI; using System.ServiceModel.Activation; namespace System.ServiceModel.Web { class CachingParameterInspector : IParameterInspector { const char seperatorChar = ';'; const char escapeChar = '\\'; const char tableDbSeperatorChar = ':'; const string invalidSqlDependencyString = "Invalid Sql dependency string."; [Fx.Tag.SecurityNote(Critical = "A config object, which should not be leaked.")] [SecurityCritical] OutputCacheProfile cacheProfile; SqlDependencyInfo[] cacheDependencyInfoArray; [Fx.Tag.SecurityNote(Critical = "Handles config objects, which should not be leaked.", Safe = "The config object never leaves the CachingParameterInspector.")] [SecuritySafeCritical] public CachingParameterInspector(string cacheProfileName) { if (string.IsNullOrEmpty(cacheProfileName)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.CacheProfileNameNullOrEmpty)); } OutputCacheSettingsSection cacheSettings = AspNetEnvironment.Current.UnsafeGetConfigurationSection("system.web/caching/outputCacheSettings") as OutputCacheSettingsSection; if (cacheSettings == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileNotConfigured, cacheProfileName))); } this.cacheProfile = cacheSettings.OutputCacheProfiles[cacheProfileName]; if (this.cacheProfile == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileNotConfigured, cacheProfileName))); } // Validate the cacheProfile if (this.cacheProfile.Location != OutputCacheLocation.None) { // Duration must be set; Duration default value is -1 if (this.cacheProfile.Duration == -1) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileValueMissing, this.cacheProfile.Name, "Duration"))); } if (this.cacheProfile.VaryByParam == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileValueMissing, this.cacheProfile.Name, "VaryByParam"))); } } if (string.Equals(this.cacheProfile.SqlDependency, "CommandNotification", StringComparison.OrdinalIgnoreCase)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new NotSupportedException(SR2.CommandNotificationSqlDependencyNotSupported)); } if (!string.IsNullOrEmpty(this.cacheProfile.SqlDependency)) { ParseSqlDependencyString(cacheProfile.SqlDependency); } } [Fx.Tag.SecurityNote(Critical = "Handles config objects, which should not be leaked.", Safe = "The config object never leaves the CachingParameterInspector.")] [SecuritySafeCritical] public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState) { if (this.cacheProfile != null && this.cacheProfile.Enabled && OperationContext.Current.IncomingMessage.Version == MessageVersion.None) { if (DiagnosticUtility.ShouldTraceWarning && !IsAnonymous()) { TraceUtility.TraceEvent(TraceEventType.Information, TraceCode.AddingAuthenticatedResponseToOutputCache, SR2.GetString(SR2.TraceCodeAddingAuthenticatedResponseToOutputCache, operationName)); } else if (DiagnosticUtility.ShouldTraceInformation) { TraceUtility.TraceEvent(TraceEventType.Information, TraceCode.AddingResponseToOutputCache, SR2.GetString(SR2.TraceCodeAddingResponseToOutputCache, operationName)); } SetCacheFromCacheProfile(); } } public object BeforeCall(string operationName, object[] inputs) { return null; } bool IsAnonymous() { if (HttpContext.Current.User.Identity.IsAuthenticated) { return false; } else { if (OperationContext.Current.ServiceSecurityContext == null) { return true; } else { return OperationContext.Current.ServiceSecurityContext.IsAnonymous; } } } static SqlDependencyInfo[] ParseSqlDependencyString(string sqlDependencyString) { // The code for this method was taken from private code in // System.Web.SqlCacheDependency.ParseSql7OutputCacheDependency. // Alter if only absolutely necessary since we want to reproduce the same ASP.NET caching behavior. List dependencyList = new List(); bool escapeSequenceFlag = false; int startIndexForDatabaseName = 0; int startIndexForTableName = -1; string databaseName = null; try { for (int currentIndex = 0; currentIndex < (sqlDependencyString.Length + 1); currentIndex++) { if (escapeSequenceFlag) { escapeSequenceFlag = false; } else if ((currentIndex != sqlDependencyString.Length) && (sqlDependencyString[currentIndex] == escapeChar)) { escapeSequenceFlag = true; } else { int subStringLength; if ((currentIndex == sqlDependencyString.Length) || (sqlDependencyString[currentIndex] == seperatorChar)) { if (databaseName == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(invalidSqlDependencyString); } subStringLength = currentIndex - startIndexForTableName; if (subStringLength == 0) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(invalidSqlDependencyString); } string tableName = sqlDependencyString.Substring(startIndexForTableName, subStringLength); SqlDependencyInfo info = new SqlDependencyInfo(); info.Database = VerifyAndRemoveEscapeCharacters(databaseName); info.Table = VerifyAndRemoveEscapeCharacters(tableName); dependencyList.Add(info); startIndexForDatabaseName = currentIndex + 1; databaseName = null; } if (currentIndex == sqlDependencyString.Length) { break; } if (sqlDependencyString[currentIndex] == tableDbSeperatorChar) { if (databaseName != null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(invalidSqlDependencyString); } subStringLength = currentIndex - startIndexForDatabaseName; if (subStringLength == 0) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(invalidSqlDependencyString); } databaseName = sqlDependencyString.Substring(startIndexForDatabaseName, subStringLength); startIndexForTableName = currentIndex + 1; } } } } catch (ArgumentException) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileSqlDependencyIsInvalid, sqlDependencyString))); } if (dependencyList.Count == 0) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR2.GetString(SR2.CacheProfileSqlDependencyIsInvalid, sqlDependencyString))); } return dependencyList.ToArray(); } static string VerifyAndRemoveEscapeCharacters(string str) { // The code for this method was taken from private code in // System.Web.SqlCacheDependency.VerifyAndRemoveEscapeCharacters. // Alter if only absolutely necessary since we want to reproduce the same ASP.NET caching behavior. bool escapeSequenceFlag = false; for (int currentIndex = 0; currentIndex < str.Length; currentIndex++) { if (escapeSequenceFlag) { if (((str[currentIndex] != escapeChar) && (str[currentIndex] != tableDbSeperatorChar)) && (str[currentIndex] != seperatorChar)) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(str); } escapeSequenceFlag = false; } else if (str[currentIndex] == escapeChar) { if ((currentIndex + 1) == str.Length) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(str); } escapeSequenceFlag = true; str = str.Remove(currentIndex, 1); currentIndex--; } } return str; } CacheDependency CreateSingleCacheDependency(string sqlDependency) { if (this.cacheDependencyInfoArray == null) { this.cacheDependencyInfoArray = CachingParameterInspector.ParseSqlDependencyString(sqlDependency); } // cacheDependencyInfoArray will never have length = 0 if (this.cacheDependencyInfoArray.Length == 1) { return new SqlCacheDependency(this.cacheDependencyInfoArray[0].Database, this.cacheDependencyInfoArray[0].Table); } AggregateCacheDependency cacheDependency = new AggregateCacheDependency(); foreach (SqlDependencyInfo dependencyInfo in this.cacheDependencyInfoArray) { cacheDependency.Add(new CacheDependency[] { new SqlCacheDependency(dependencyInfo.Database, dependencyInfo.Table) }); } return cacheDependency; } [Fx.Tag.SecurityNote(Critical = "Uses config object to set properties of the HttpCachePolicy.", Safe = "The config object itself doesn't leak.")] [SecuritySafeCritical] void SetCacheFromCacheProfile() { HttpCachePolicy cache = HttpContext.Current.Response.Cache; if (this.cacheProfile.NoStore) { cache.SetNoStore(); } // Location is not required to be set in the config. The default is Any, // but if it is not set in the config the value will be -1. So must correct for this. if ((int)(this.cacheProfile.Location) == -1) { cache.SetCacheability(HttpCacheability.Public); } else { switch (this.cacheProfile.Location) { case OutputCacheLocation.Any: cache.SetCacheability(HttpCacheability.Public); break; case OutputCacheLocation.Client: cache.SetCacheability(HttpCacheability.Private); break; case OutputCacheLocation.Downstream: cache.SetCacheability(HttpCacheability.Public); cache.SetNoServerCaching(); break; case OutputCacheLocation.None: cache.SetCacheability(HttpCacheability.NoCache); break; case OutputCacheLocation.Server: cache.SetCacheability(HttpCacheability.ServerAndNoCache); break; case OutputCacheLocation.ServerAndClient: cache.SetCacheability(HttpCacheability.ServerAndPrivate); break; default: throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new NotSupportedException(SR2.GetString(SR2.CacheProfileLocationNotSupported, this.cacheProfile.Location))); } } if (this.cacheProfile.Location != OutputCacheLocation.None) { cache.SetExpires(HttpContext.Current.Timestamp.AddSeconds((double)this.cacheProfile.Duration)); cache.SetMaxAge(new TimeSpan(0, 0, this.cacheProfile.Duration)); cache.SetValidUntilExpires(true); cache.SetLastModified(HttpContext.Current.Timestamp); if (this.cacheProfile.Location != OutputCacheLocation.Client) { if (!string.IsNullOrEmpty(this.cacheProfile.VaryByContentEncoding)) { foreach (string contentEncoding in this.cacheProfile.VaryByContentEncoding.Split(seperatorChar)) { cache.VaryByContentEncodings[contentEncoding.Trim()] = true; } } if (!string.IsNullOrEmpty(this.cacheProfile.VaryByHeader)) { foreach (string header in this.cacheProfile.VaryByHeader.Split(seperatorChar)) { cache.VaryByHeaders[header.Trim()] = true; } } if (this.cacheProfile.Location != OutputCacheLocation.Downstream) { if (!string.IsNullOrEmpty(this.cacheProfile.VaryByCustom)) { cache.SetVaryByCustom(this.cacheProfile.VaryByCustom); } if (!string.IsNullOrEmpty(this.cacheProfile.VaryByParam)) { foreach (string parameter in cacheProfile.VaryByParam.Split(seperatorChar)) { cache.VaryByParams[parameter.Trim()] = true; } } if (!string.IsNullOrEmpty(this.cacheProfile.SqlDependency)) { CacheDependency cacheDependency = this.CreateSingleCacheDependency(cacheProfile.SqlDependency); HttpContext.Current.Response.AddCacheDependency(new CacheDependency[] { cacheDependency }); } } } } } private struct SqlDependencyInfo { public string Database; public string Table; } } }