2 // System.Web.Handlers.AssemblyResourceLoader
5 // Ben Maurer (bmaurer@users.sourceforge.net)
6 // Marek Habersack <grendel@twistedcode.net>
9 // (C) 2010 Novell, Inc (http://novell.com/)
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.Globalization;
34 using System.Reflection;
36 using System.Resources;
37 using System.Collections;
38 using System.Collections.Generic;
39 using System.Security.Cryptography;
41 using System.Text.RegularExpressions;
42 using System.Threading;
43 using System.Web.Configuration;
44 using System.Web.Util;
46 namespace System.Web.Handlers
48 #if SYSTEM_WEB_EXTENSIONS
49 partial class ScriptResourceHandler
51 const string HandlerFileName = "ScriptResource.axd";
52 static Assembly currAsm = typeof (ScriptResourceHandler).Assembly;
57 internal // since this is in the .config file, we need to support it, since we dont have versoned support.
59 class AssemblyResourceLoader : IHttpHandler
61 const string HandlerFileName = "WebResource.axd";
62 static Assembly currAsm = typeof (AssemblyResourceLoader).Assembly;
64 const char QueryParamSeparator = '&';
66 static readonly Dictionary <string, AssemblyEmbeddedResources> _embeddedResources = new Dictionary <string, AssemblyEmbeddedResources> (StringComparer.Ordinal);
67 static readonly ReaderWriterLockSlim _embeddedResourcesLock = new ReaderWriterLockSlim ();
68 static readonly ReaderWriterLockSlim _stringHashCacheLock = new ReaderWriterLockSlim ();
69 static readonly Dictionary <string, string> stringHashCache = new Dictionary <string, string> (StringComparer.Ordinal);
72 static KeyedHashAlgorithm hashAlg;
73 static bool canReuseHashAlg = true;
75 static KeyedHashAlgorithm ReusableHashAlgorithm {
80 if (hashAlg == null) {
81 MachineKeySection mks = MachineKeySection.Config;
82 hashAlg = MachineKeySectionUtils.GetValidationAlgorithm (mks);
83 if (!hashAlg.CanReuseTransform) {
84 canReuseHashAlg = false;
90 hashAlg.Initialize ();
96 static string GetStringHash (KeyedHashAlgorithm kha, string str)
98 if (String.IsNullOrEmpty (str))
103 _stringHashCacheLock.EnterUpgradeableReadLock ();
104 if (stringHashCache.TryGetValue (str, out result))
108 _stringHashCacheLock.EnterWriteLock ();
109 if (stringHashCache.TryGetValue (str, out result))
112 result = Convert.ToBase64String (kha.ComputeHash (Encoding.UTF8.GetBytes (str)));
113 stringHashCache.Add (str, result);
115 _stringHashCacheLock.ExitWriteLock ();
118 _stringHashCacheLock.ExitUpgradeableReadLock ();
124 static void InitEmbeddedResourcesUrls (KeyedHashAlgorithm kha, Assembly assembly, string assemblyName, string assemblyHash, AssemblyEmbeddedResources entry)
126 WebResourceAttribute [] attrs = (WebResourceAttribute []) assembly.GetCustomAttributes (typeof (WebResourceAttribute), false);
127 WebResourceAttribute attr;
128 string apath = assembly.Location;
129 for (int i = 0; i < attrs.Length; i++) {
131 string resourceName = attr.WebResource;
132 if (!String.IsNullOrEmpty (resourceName)) {
133 string resourceNameHash = GetStringHash (kha, resourceName);
134 #if SYSTEM_WEB_EXTENSIONS
135 bool debug = resourceName.EndsWith (".debug.js", StringComparison.OrdinalIgnoreCase);
136 string dbgTail = debug ? "d" : String.Empty;
137 string rkNoNotify = resourceNameHash + "f" + dbgTail;
138 string rkNotify = resourceNameHash + "t" + dbgTail;
140 if (!entry.Resources.ContainsKey (rkNoNotify)) {
141 var er = new EmbeddedResource () {
144 Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, rkNoNotify, debug, false)
147 entry.Resources.Add (rkNoNotify, er);
150 if (!entry.Resources.ContainsKey (rkNotify)) {
151 var er = new EmbeddedResource () {
154 Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, rkNotify, debug, true)
157 entry.Resources.Add (rkNotify, er);
160 if (!entry.Resources.ContainsKey (resourceNameHash)) {
161 var er = new EmbeddedResource () {
164 Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, resourceNameHash, false, false)
166 entry.Resources.Add (resourceNameHash, er);
173 #if !SYSTEM_WEB_EXTENSIONS
174 internal static string GetResourceUrl (Type type, string resourceName)
176 return GetResourceUrl (type.Assembly, resourceName, false);
179 static EmbeddedResource DecryptAssemblyResource (string val, out AssemblyEmbeddedResources entry)
183 string[] parts = val.Split ('_');
184 if (parts.Length != 3)
187 Encoding enc = Encoding.UTF8;
188 string asmNameHash = parts [0];
189 string resNameHash = parts [1];
190 bool debug = parts [2] == "t";
193 _embeddedResourcesLock.EnterReadLock ();
194 if (!_embeddedResources.TryGetValue (asmNameHash, out entry) || entry == null)
197 EmbeddedResource res;
198 if (!entry.Resources.TryGetValue (resNameHash, out res) || res == null) {
199 #if SYSTEM_WEB_EXTENSIONS
203 if (!entry.Resources.TryGetValue (resNameHash.Substring (0, resNameHash.Length - 1), out res))
212 _embeddedResourcesLock.ExitReadLock ();
216 internal static string GetResourceUrl (Assembly assembly, string resourceName, bool notifyScriptLoaded)
218 if (assembly == null)
221 KeyedHashAlgorithm kha = ReusableHashAlgorithm;
223 return GetResourceUrl (kha, assembly, resourceName, notifyScriptLoaded);
225 MachineKeySection mks = MachineKeySection.Config;
226 using (kha = MachineKeySectionUtils.GetValidationAlgorithm (mks)) {
227 kha.Key = MachineKeySectionUtils.GetValidationKey (mks);
228 return GetResourceUrl (kha, assembly, resourceName, notifyScriptLoaded);
233 static string GetResourceUrl (KeyedHashAlgorithm kha, Assembly assembly, string resourceName, bool notifyScriptLoaded)
235 string assemblyName = assembly == currAsm ? "s" : assembly.GetName ().FullName;
236 string assemblyNameHash = GetStringHash (kha, assemblyName);
237 string resourceNameHash = GetStringHash (kha, resourceName);
240 AssemblyEmbeddedResources entry;
243 _embeddedResourcesLock.EnterUpgradeableReadLock ();
244 if (!_embeddedResources.TryGetValue (assemblyNameHash, out entry) || entry == null) {
246 _embeddedResourcesLock.EnterWriteLock ();
247 entry = new AssemblyEmbeddedResources () {
248 AssemblyName = assemblyName
250 InitEmbeddedResourcesUrls (kha, assembly, assemblyName, assemblyNameHash, entry);
251 _embeddedResources.Add (assemblyNameHash, entry);
253 _embeddedResourcesLock.ExitWriteLock ();
257 #if SYSTEM_WEB_EXTENSIONS
258 debug = resourceName.EndsWith (".debug.js", StringComparison.OrdinalIgnoreCase);
259 string dbgTail = debug ? "d" : String.Empty;
260 lookupKey = resourceNameHash + (notifyScriptLoaded ? "t" : "f") + dbgTail;
262 lookupKey = resourceNameHash;
264 EmbeddedResource res;
265 if (entry.Resources.TryGetValue (lookupKey, out res) && res != null)
268 #if SYSTEM_WEB_EXTENSIONS
270 resourceNameHash = GetStringHash (kha, resourceName.Substring (0, resourceName.Length - 9) + ".js");
271 lookupKey = resourceNameHash + (notifyScriptLoaded ? "t" : "f");
273 if (entry.Resources.TryGetValue (lookupKey, out res) && res != null)
282 _embeddedResourcesLock.ExitUpgradeableReadLock ();
286 url = CreateResourceUrl (kha, assemblyName, assemblyNameHash, assembly.Location, resourceNameHash, debug, notifyScriptLoaded);
291 static string CreateResourceUrl (KeyedHashAlgorithm kha, string assemblyName, string assemblyNameHash, string assemblyPath, string resourceNameHash, bool debug, bool notifyScriptLoaded)
293 string atime = String.Empty;
294 string extra = String.Empty;
295 #if SYSTEM_WEB_EXTENSIONS
296 extra = QueryParamSeparator + "n=" + (notifyScriptLoaded ? "t" : "f");
300 atime = QueryParamSeparator + "t=" + assemblyName.GetHashCode ();
302 if (!String.IsNullOrEmpty (assemblyPath) && File.Exists (assemblyPath))
303 atime = QueryParamSeparator + "t=" + File.GetLastWriteTimeUtc (assemblyPath).Ticks;
305 atime = QueryParamSeparator + "t=" + DateTime.UtcNow.Ticks;
307 string d = assemblyNameHash + "_" + resourceNameHash + (debug ? "_t" : "_f");
308 string href = HandlerFileName + "?d=" + d + atime + extra;
309 HttpContext ctx = HttpContext.Current;
310 HttpRequest req = ctx != null ? ctx.Request : null;
312 string appPath = VirtualPathUtility.AppendTrailingSlash (req.ApplicationPath);
313 href = appPath + href;
319 #if SYSTEM_WEB_EXTENSIONS
320 protected virtual void ProcessRequest (HttpContext context)
322 void System.Web.IHttpHandler.ProcessRequest (HttpContext context)
325 HttpRequest request = context.Request;
326 // val is URL-encoded, which means every + has been replaced with ' ', we
327 // need to revert that or the base64 conversion will fail.
328 string d = request.QueryString ["d"];
329 if (!String.IsNullOrEmpty (d))
330 d = d.Replace (' ', '+');
332 AssemblyEmbeddedResources entry;
333 EmbeddedResource res = DecryptAssemblyResource (d, out entry);
334 WebResourceAttribute wra = res != null ? res.Attribute : null;
336 throw new HttpException (404, "Resource not found");
339 if (entry.AssemblyName == "s")
342 assembly = Assembly.Load (entry.AssemblyName);
344 HttpResponse response = context.Response;
345 string req_cache = request.Headers ["Cache-Control"];
346 if (String.Compare (req_cache, "max-age=0", StringComparison.Ordinal) == 0) {
348 if (Int64.TryParse (request.QueryString ["t"], out atime)) {
349 if (atime == File.GetLastWriteTimeUtc (assembly.Location).Ticks) {
351 response.StatusCode = 304;
352 response.ContentType = null;
353 response.CacheControl = "public"; // easier to set it to public as MS than remove it
354 context.ApplicationInstance.CompleteRequest ();
359 string modif_since = request.Headers ["If-Modified-Since"];
360 if (!String.IsNullOrEmpty (modif_since)) {
363 if (DateTime.TryParseExact (modif_since, "r", null, 0, out modif)) {
364 if (File.GetLastWriteTimeUtc (assembly.Location) <= modif) {
366 response.StatusCode = 304;
367 response.ContentType = null;
368 response.CacheControl = "public"; // easier to set it to public as MS than remove it
369 context.ApplicationInstance.CompleteRequest ();
376 response.ContentType = wra.ContentType;
378 DateTime utcnow = DateTime.UtcNow;
379 response.Headers.Add ("Last-Modified", utcnow.ToString ("r"));
380 response.ExpiresAbsolute = utcnow.AddYears (1);
381 response.CacheControl = "public";
383 Stream s = assembly.GetManifestResourceStream (res.Name);
385 throw new HttpException (404, "Resource " + res.Name + " not found");
387 if (wra.PerformSubstitution) {
388 using (StreamReader r = new StreamReader (s)) {
389 TextWriter w = response.Output;
390 new PerformSubstitutionHelper (assembly).PerformSubstitution (r, w);
392 } else if (response.OutputStream is HttpResponseStream) {
393 UnmanagedMemoryStream st = (UnmanagedMemoryStream) s;
394 HttpResponseStream hstream = (HttpResponseStream) response.OutputStream;
396 hstream.WritePtr (new IntPtr (st.PositionPointer), (int) st.Length);
399 byte [] buf = new byte [1024];
400 Stream output = response.OutputStream;
403 c = s.Read (buf, 0, 1024);
404 output.Write (buf, 0, c);
407 #if SYSTEM_WEB_EXTENSIONS
408 TextWriter writer = response.Output;
409 foreach (ScriptResourceAttribute sra in assembly.GetCustomAttributes (typeof (ScriptResourceAttribute), false)) {
410 if (String.Compare (sra.ScriptName, res.Name, StringComparison.Ordinal) == 0) {
411 string scriptResourceName = sra.ScriptResourceName;
412 ResourceSet rset = null;
414 rset = new ResourceManager (scriptResourceName, assembly).GetResourceSet (Threading.Thread.CurrentThread.CurrentUICulture, true, true);
416 catch (MissingManifestResourceException) {
417 #if TARGET_JVM // GetResourceSet does not throw MissingManifestResourceException if ressource is not exists
421 if (scriptResourceName.EndsWith (".resources")) {
422 scriptResourceName = scriptResourceName.Substring (0, scriptResourceName.Length - 10);
423 rset = new ResourceManager (scriptResourceName, assembly).GetResourceSet (Threading.Thread.CurrentThread.CurrentUICulture, true, true);
433 string ns = sra.TypeName;
434 int indx = ns.LastIndexOf ('.');
436 writer.WriteLine ("Type.registerNamespace('" + ns.Substring (0, indx) + "')");
437 writer.Write ("{0}={{", sra.TypeName);
439 foreach (DictionaryEntry de in rset) {
440 string value = de.Value as string;
447 writer.Write ("{0}:{1}", GetScriptStringLiteral ((string) de.Key), GetScriptStringLiteral (value));
451 writer.WriteLine ("};");
456 bool notifyScriptLoaded = request.QueryString ["n"] == "t";
457 if (notifyScriptLoaded) {
459 writer.WriteLine ("if(typeof(Sys)!=='undefined')Sys.Application.notifyScriptLoaded();");
464 sealed class PerformSubstitutionHelper
466 readonly Assembly _assembly;
467 static readonly Regex _regex = new Regex (@"\<%=[ ]*WebResource[ ]*\([ ]*""([^""]+)""[ ]*\)[ ]*%\>");
469 public PerformSubstitutionHelper (Assembly assembly) {
470 _assembly = assembly;
473 public void PerformSubstitution (TextReader reader, TextWriter writer) {
474 string line = reader.ReadLine ();
475 while (line != null) {
476 if (line.Length > 0 && _regex.IsMatch (line))
477 line = _regex.Replace (line, new MatchEvaluator (PerformSubstitutionReplace));
478 writer.WriteLine (line);
479 line = reader.ReadLine ();
483 string PerformSubstitutionReplace (Match m) {
484 string resourceName = m.Groups [1].Value;
485 #if SYSTEM_WEB_EXTENSIONS
486 return ScriptResourceHandler.GetResourceUrl (_assembly, resourceName, false);
488 return AssemblyResourceLoader.GetResourceUrl (_assembly, resourceName, false);
493 #if !SYSTEM_WEB_EXTENSIONS
494 bool System.Web.IHttpHandler.IsReusable { get { return true; } }
496 sealed class EmbeddedResource
500 public WebResourceAttribute Attribute;
503 sealed class AssemblyEmbeddedResources
505 public string AssemblyName = String.Empty;
506 public Dictionary <string, EmbeddedResource> Resources = new Dictionary <string, EmbeddedResource> (StringComparer.Ordinal);