e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
367 lines
16 KiB
C#
367 lines
16 KiB
C#
//-----------------------------------------------------------------------------
|
|
// 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<SqlDependencyInfo> dependencyList = new List<SqlDependencyInfo>();
|
|
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;
|
|
}
|
|
|
|
}
|
|
}
|