//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//------------------------------------------------------------------------------
namespace System.Web.Handlers {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Resources;
using System.Security;
using System.Security.Cryptography;
using System.Security.Policy;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Resources;
using System.Web.Security.Cryptography;
using System.Web.UI;
using System.Web.Util;
using Debug = System.Diagnostics.Debug;
public class ScriptResourceHandler : IHttpHandler {
private const string _scriptResourceUrl = "~/ScriptResource.axd";
private static readonly IDictionary _assemblyInfoCache = Hashtable.Synchronized(new Hashtable());
private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
private static readonly Object _getMethodLock = new Object();
private static IScriptResourceHandler _scriptResourceHandler = new RuntimeScriptResourceHandler();
private static string _scriptResourceAbsolutePath;
// _bypassVirtualPathResolution set by unit tests to avoid resolving ~/ paths from unit tests.
private static bool _bypassVirtualPathResolution = false;
private static int _maximumResourceUrlLength = 2048;
private static string ScriptResourceAbsolutePath {
get {
if (_scriptResourceAbsolutePath == null) {
_scriptResourceAbsolutePath = VirtualPathUtility.ToAbsolute(_scriptResourceUrl);
}
return _scriptResourceAbsolutePath;
}
}
private static Exception Create404(Exception innerException) {
return new HttpException(404, AtlasWeb.ScriptResourceHandler_InvalidRequest, innerException);
}
internal static CultureInfo DetermineNearestAvailableCulture(
Assembly assembly,
string scriptResourceName,
CultureInfo culture) {
if (String.IsNullOrEmpty(scriptResourceName)) return CultureInfo.InvariantCulture;
Tuple cacheKey = Tuple.Create(assembly, scriptResourceName, culture);
CultureInfo cachedCulture = (CultureInfo)_cultureCache[cacheKey];
if (cachedCulture == null) {
string releaseResourceName =
scriptResourceName.EndsWith(".debug.js", StringComparison.OrdinalIgnoreCase) ?
scriptResourceName.Substring(0, scriptResourceName.Length - 9) + ".js" :
null;
ScriptResourceInfo resourceInfo = ScriptResourceInfo.GetInstance(assembly, scriptResourceName);
ScriptResourceInfo releaseResourceInfo = (releaseResourceName != null) ?
ScriptResourceInfo.GetInstance(assembly, releaseResourceName) : null;
if (!String.IsNullOrEmpty(resourceInfo.ScriptResourceName) ||
((releaseResourceInfo != null) && !String.IsNullOrEmpty(releaseResourceInfo.ScriptResourceName))) {
ResourceManager resourceManager =
ScriptResourceAttribute.GetResourceManager(resourceInfo.ScriptResourceName, assembly);
ResourceManager releaseResourceManager = (releaseResourceInfo != null) ?
ScriptResourceAttribute.GetResourceManager(releaseResourceInfo.ScriptResourceName, assembly) : null;
ResourceSet localizedSet = null;
ResourceSet releaseSet = null;
if (resourceManager != null) {
resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
// Look for the explicitly localized version of the resources that is nearest the culture.
localizedSet = resourceManager.GetResourceSet(culture, true, false);
}
if (releaseResourceManager != null) {
releaseResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
// Look for the explicitly localized version of the resources that is nearest the culture.
releaseSet = releaseResourceManager.GetResourceSet(culture, true, false);
}
if ((resourceManager != null) || (releaseResourceManager != null)) {
while ((localizedSet == null) && (releaseSet == null)) {
culture = culture.Parent;
if (culture.Equals(CultureInfo.InvariantCulture)) break;
localizedSet = resourceManager.GetResourceSet(culture, true, false);
releaseSet = (releaseResourceManager != null) ?
releaseResourceManager.GetResourceSet(culture, true, false) : null;
}
}
else {
culture = CultureInfo.InvariantCulture;
}
}
else {
culture = CultureInfo.InvariantCulture;
}
// Neutral assembly culture falls back on invariant
CultureInfo neutralCulture = GetAssemblyNeutralCulture(assembly);
if ((neutralCulture != null) && neutralCulture.Equals(culture)) {
culture = CultureInfo.InvariantCulture;
}
cachedCulture = culture;
_cultureCache[cacheKey] = cachedCulture;
}
return cachedCulture;
}
private static void EnsureScriptResourceRequest(string path) {
if (!IsScriptResourceRequest(path)) {
Throw404();
}
}
private static Assembly GetAssembly(string assemblyName) {
Debug.Assert(!String.IsNullOrEmpty(assemblyName));
string[] parts = assemblyName.Split(',');
if ((parts.Length != 1) && (parts.Length != 4)) {
Throw404();
}
AssemblyName realName = new AssemblyName();
realName.Name = parts[0];
if (parts.Length == 4) {
realName.Version = new Version(parts[1]);
string cultureString = parts[2];
realName.CultureInfo = (cultureString.Length > 0) ?
new CultureInfo(cultureString) :
CultureInfo.InvariantCulture;
realName.SetPublicKeyToken(HexParser.Parse(parts[3]));
}
Assembly assembly = null;
try {
assembly = Assembly.Load(realName);
}
catch (FileNotFoundException fnf) {
Throw404(fnf);
}
catch (FileLoadException fl) {
Throw404(fl);
}
catch (BadImageFormatException badImage) {
Throw404(badImage);
}
return assembly;
}
private static Tuple GetAssemblyInfo(Assembly assembly) {
Tuple assemblyInfo =
(Tuple)_assemblyInfoCache[assembly];
if (assemblyInfo == null) {
assemblyInfo = GetAssemblyInfoInternal(assembly);
_assemblyInfoCache[assembly] = assemblyInfo;
}
Debug.Assert(assemblyInfo != null, "Assembly info should not be null");
return assemblyInfo;
}
private static Tuple GetAssemblyInfoInternal(Assembly assembly) {
AssemblyName assemblyName = new AssemblyName(assembly.FullName);
string hash = Convert.ToBase64String(assembly.ManifestModule.ModuleVersionId.ToByteArray());
return new Tuple(assemblyName, hash);
}
private static CultureInfo GetAssemblyNeutralCulture(Assembly assembly) {
CultureInfo neutralCulture = (CultureInfo)_cultureCache[assembly];
if (neutralCulture == null) {
object[] nrlas = assembly.GetCustomAttributes(typeof(NeutralResourcesLanguageAttribute), false);
if ((nrlas != null) && (nrlas.Length != 0)) {
neutralCulture = CultureInfo.GetCultureInfo(
((NeutralResourcesLanguageAttribute)nrlas[0]).CultureName);
_cultureCache[assembly] = neutralCulture;
}
}
return neutralCulture;
}
internal static string GetEmptyPageUrl(string title) {
return GetScriptResourceHandler().GetEmptyPageUrl(title);
}
private static IScriptResourceHandler GetScriptResourceHandler() {
if (_scriptResourceHandler == null) {
_scriptResourceHandler = new RuntimeScriptResourceHandler();
}
return _scriptResourceHandler;
}
internal static string GetScriptResourceUrl(
Assembly assembly,
string resourceName,
CultureInfo culture,
bool zip) {
return GetScriptResourceHandler()
.GetScriptResourceUrl(assembly, resourceName, culture, zip);
}
internal static string GetScriptResourceUrl(
List>>> assemblyResourceLists,
bool zip) {
return GetScriptResourceHandler().GetScriptResourceUrl(assemblyResourceLists, zip);
}
protected virtual bool IsReusable {
get {
return true;
}
}
internal delegate string VirtualFileReader(string virtualPath, out Encoding encoding);
private static bool IsCompressionEnabled(HttpContext context) {
return ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCompression &&
((context == null) ||
!context.Request.Browser.IsBrowser("IE") ||
(context.Request.Browser.MajorVersion > 6));
}
internal static bool IsScriptResourceRequest(string path) {
return !String.IsNullOrEmpty(path) &&
String.Equals(path, ScriptResourceAbsolutePath, StringComparison.OrdinalIgnoreCase);
}
private static void OutputEmptyPage(HttpResponseBase response, string title) {
PrepareResponseCache(response);
response.Write(@"
" +
HttpUtility.HtmlEncode(title) +
@"");
}
private static void PrepareResponseCache(HttpResponseBase response) {
HttpCachePolicyBase cachePolicy = response.Cache;
DateTime now = DateTime.Now;
cachePolicy.SetCacheability(HttpCacheability.Public);
cachePolicy.VaryByParams["d"] = true;
cachePolicy.SetOmitVaryStar(true);
cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
cachePolicy.SetValidUntilExpires(true);
cachePolicy.SetLastModified(now);
}
private static void PrepareResponseNoCache(HttpResponseBase response) {
HttpCachePolicyBase cachePolicy = response.Cache;
DateTime now = DateTime.Now;
cachePolicy.SetCacheability(HttpCacheability.Public);
cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
cachePolicy.SetValidUntilExpires(true);
cachePolicy.SetLastModified(now);
cachePolicy.SetNoServerCaching();
}
[SecuritySafeCritical]
protected virtual void ProcessRequest(HttpContext context) {
ProcessRequest(new HttpContextWrapper(context));
}
internal static void ProcessRequest(HttpContextBase context, VirtualFileReader fileReader = null, Action logAction = null, bool validatePath = true) {
string decryptedString = null;
bool resourceIdentifierPresent = false;
try {
HttpResponseBase response = context.Response;
response.Clear();
if (validatePath) {
// Checking that the handler is not being called from a different path.
EnsureScriptResourceRequest(context.Request.Path);
}
string encryptedData = context.Request.QueryString["d"];
if (String.IsNullOrEmpty(encryptedData)) {
Throw404();
}
resourceIdentifierPresent = true;
try {
decryptedString = Page.DecryptString(encryptedData, Purpose.ScriptResourceHandler_ScriptResourceUrl);
}
catch (CryptographicException ex) {
Throw404(ex);
}
fileReader = fileReader ?? new VirtualFileReader(delegate(string virtualPath, out Encoding encoding) {
VirtualPathProvider vpp = HostingEnvironment.VirtualPathProvider;
if (!vpp.FileExists(virtualPath)) {
Throw404();
}
VirtualFile file = vpp.GetFile(virtualPath);
if (!AppSettings.ScriptResourceAllowNonJsFiles && !file.Name.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) {
// MSRC 10405: Disallow all extensions other than *.js
Throw404();
}
using (Stream stream = file.Open()) {
using (StreamReader reader = new StreamReader(stream, true)) {
encoding = reader.CurrentEncoding;
return reader.ReadToEnd();
}
}
});
ProcessRequestInternal(response, decryptedString, fileReader);
} catch(Exception e) {
if (resourceIdentifierPresent) {
logAction = logAction ?? AssemblyResourceLoader.LogWebResourceFailure;
logAction(decryptedString, e);
}
// MSRC 10405: There's no reason for this to return anything other than a 404 if something
// goes wrong. We shouldn't propagate the inner exception inside the YSOD, as it might
// contain sensitive cryptographic information.
Throw404();
}
}
private static void ProcessRequestInternal(
HttpResponseBase response,
string decryptedString,
VirtualFileReader fileReader) {
if (String.IsNullOrEmpty(decryptedString)) {
Throw404();
}
bool zip;
bool singleAssemblyReference;
// See GetScriptResourceUrl comment below for first character meanings.
switch (decryptedString[0]) {
case 'Z':
case 'z':
singleAssemblyReference = true;
zip = true;
break;
case 'U':
case 'u':
singleAssemblyReference = true;
zip = false;
break;
case 'Q':
case 'q':
singleAssemblyReference = false;
zip = true;
break;
case 'R':
case 'r':
singleAssemblyReference = false;
zip = false;
break;
case 'T':
OutputEmptyPage(response, decryptedString.Substring(1));
return;
default:
Throw404();
return;
}
decryptedString = decryptedString.Substring(1);
if (String.IsNullOrEmpty(decryptedString)) {
Throw404();
}
string[] decryptedData = decryptedString.Split('|');
if (singleAssemblyReference) {
// expected: ||[|#|]
if (decryptedData.Length != 3 && decryptedData.Length != 5) {
// The decrypted data must have 3 parts plus an optional 2 part hash code separated by pipes.
Throw404();
}
}
else {
// expected: |,,,,...||,,,,...|#|
if (decryptedData.Length % 2 != 0) {
// The decrypted data must have an even number of parts separated by pipes.
Throw404();
}
}
StringBuilder script = new StringBuilder();
string firstContentType = null;
if (singleAssemblyReference) {
// single assembly reference, format is
// ||
string assemblyName = decryptedData[0];
string resourceName = decryptedData[1];
string cultureName = decryptedData[2];
Assembly assembly = GetAssembly(assemblyName);
if (assembly == null) {
Throw404();
}
script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
assembly,
resourceName,
String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
zip,
out firstContentType
));
}
else {
// composite script reference, format is:
// |,,,,...||,,,,...
// Assembly is empty for path based scripts, and their resource/culture list is ,,...
// If an assembly starts with "#", the segment is ignored (expected that this includes a hash to ensure
// url uniqueness when resources are changed). Also, for forward compatibility '#' segments may contain
// other data.
bool needsNewline = false;
for (int i = 0; i < decryptedData.Length; i += 2) {
string assemblyName = decryptedData[i];
bool hasAssembly = !String.IsNullOrEmpty(assemblyName);
if (hasAssembly && assemblyName[0] == '#') {
// hash segments are ignored, it contains a hash code for url uniqueness
continue;
}
Debug.Assert(!String.IsNullOrEmpty(decryptedData[i + 1]));
string[] resourcesAndCultures = decryptedData[i + 1].Split(',');
if (resourcesAndCultures.Length == 0) {
Throw404();
}
Assembly assembly = hasAssembly ? GetAssembly(assemblyName) : null;
if (assembly == null) {
// The scripts are path-based
if (firstContentType == null) {
firstContentType = "text/javascript";
}
for (int j = 0; j < resourcesAndCultures.Length; j++) {
Encoding encoding;
// DevDiv Bugs 197242
// path will either be absolute, as in "/app/foo/bar.js" or app relative, as in "~/foo/bar.js"
// ToAbsolute() ensures it is in the form /app/foo/bar.js
// This conversion was not done when the url was created to conserve url length.
string path = _bypassVirtualPathResolution ?
resourcesAndCultures[j] :
VirtualPathUtility.ToAbsolute(resourcesAndCultures[j]);
string fileContents = fileReader(path, out encoding);
if (needsNewline) {
// Output an additional newline between resources but not for the last one
script.Append('\n');
}
needsNewline = true;
script.Append(fileContents);
}
}
else {
Debug.Assert(resourcesAndCultures.Length % 2 == 0, "The list of resource names and cultures must have an even number of parts separated by commas.");
for (int j = 0; j < resourcesAndCultures.Length; j += 2) {
try {
string contentType;
string resourceName = resourcesAndCultures[j];
string cultureName = resourcesAndCultures[j + 1];
if (needsNewline) {
// Output an additional newline between resources but not for the last one
script.Append('\n');
}
needsNewline = true;
script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
assembly,
resourceName,
String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
zip,
out contentType
));
if (firstContentType == null) {
firstContentType = contentType;
}
}
catch (MissingManifestResourceException ex) {
throw Create404(ex);
}
catch (HttpException ex) {
throw Create404(ex);
}
}
}
}
}
if (ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCaching) {
PrepareResponseCache(response);
}
else {
PrepareResponseNoCache(response);
}
response.ContentType = firstContentType;
if (zip) {
using (MemoryStream zipped = new MemoryStream()) {
using (Stream outputStream = new GZipStream(zipped, CompressionMode.Compress)) {
// The choice of an encoding matters little here.
// Input streams being of potentially different encodings, UTF-8 is the better
// choice as it's the natural encoding for JavaScript.
using (StreamWriter writer = new StreamWriter(outputStream, Encoding.UTF8)) {
writer.Write(script.ToString());
}
}
byte[] zippedBytes = zipped.ToArray();
response.AddHeader("Content-encoding", "gzip");
response.OutputStream.Write(zippedBytes, 0, zippedBytes.Length);
}
}
else {
// Bug DevDiv #175061, we don't want to force any encoding here and let the default
// encoding apply no matter what the incoming scripts might have been encoded with.
response.Write(script.ToString());
}
}
internal static void SetScriptResourceHandler(IScriptResourceHandler scriptResourceHandler) {
_scriptResourceHandler = scriptResourceHandler;
}
private static void Throw404() {
throw Create404(null);
}
private static void Throw404(Exception innerException) {
throw Create404(innerException);
}
#region IHttpHandler implementation
void IHttpHandler.ProcessRequest(HttpContext context) {
ProcessRequest(context);
}
bool IHttpHandler.IsReusable {
get {
return IsReusable;
}
}
#endregion
private class RuntimeScriptResourceHandler : IScriptResourceHandler {
// Keys in the URL cache will be IList objects, so use our custom list comparer.
private static readonly IDictionary _urlCache = Hashtable.Synchronized(new Hashtable(ListEqualityComparer.Instance));
private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
private static string _absoluteScriptResourceUrl;
string IScriptResourceHandler.GetScriptResourceUrl(
Assembly assembly, string resourceName, CultureInfo culture, bool zip) {
return ((IScriptResourceHandler)this).GetScriptResourceUrl(
new List>>>() {
new Tuple>>(
assembly,
new List>() {
new Tuple(resourceName, culture)
}
)
}, zip);
}
string IScriptResourceHandler.GetScriptResourceUrl(
List>>> assemblyResourceLists,
bool zip) {
if (!IsCompressionEnabled(HttpContext.Current)) {
zip = false;
}
bool allAssemblyResources = true;
foreach (Tuple>> assemblyData in assemblyResourceLists) {
if (assemblyData.Item1 == null) {
allAssemblyResources = false;
break;
}
}
// If all the scripts are assembly resources, we can cache the generated ScriptResource URL, since
// the appdomain will reset if any of the assemblies are changed. We cannot cache the URL if any
// scripts are path-based, since the cache entry will not be removed if a path-based script is changed.
if (allAssemblyResources) {
List