Merge pull request #896 from echampet/webresource
[mono.git] / mcs / class / System.Web / System.Web.Handlers / AssemblyResourceLoader.cs
1 //
2 // System.Web.Handlers.AssemblyResourceLoader
3 //
4 // Authors:
5 //      Ben Maurer (bmaurer@users.sourceforge.net)
6 //      Marek Habersack <grendel@twistedcode.net>
7 //
8 // (C) 2003 Ben Maurer
9 // (C) 2010 Novell, Inc (http://novell.com/)
10
11 //
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:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
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.
30 //
31
32 using System.Web.UI;
33 using System.Globalization;
34 using System.Reflection;
35 using System.IO;
36 using System.Resources;
37 using System.Collections;
38 using System.Collections.Generic;
39 using System.Collections.Specialized;
40 using System.Security.Cryptography;
41 using System.Text;
42 using System.Text.RegularExpressions;
43 using System.Threading;
44 using System.Web.Configuration;
45 using System.Web.Util;
46
47 namespace System.Web.Handlers
48 {
49 #if SYSTEM_WEB_EXTENSIONS
50         partial class ScriptResourceHandler
51         {
52                 const string HandlerFileName = "ScriptResource.axd";
53                 static Assembly currAsm = typeof (ScriptResourceHandler).Assembly;
54 #else
55         public sealed class AssemblyResourceLoader : IHttpHandler
56         {
57                 const string HandlerFileName = "WebResource.axd";
58                 static Assembly currAsm = typeof (AssemblyResourceLoader).Assembly;
59 #endif
60                 const char QueryParamSeparator = '&';
61
62                 static readonly Dictionary <string, AssemblyEmbeddedResources> _embeddedResources = new Dictionary <string, AssemblyEmbeddedResources> (StringComparer.Ordinal);
63                 static readonly ReaderWriterLockSlim _embeddedResourcesLock = new ReaderWriterLockSlim ();
64                 static readonly ReaderWriterLockSlim _stringHashCacheLock = new ReaderWriterLockSlim ();
65                 static readonly Dictionary <string, string> stringHashCache = new Dictionary <string, string> (StringComparer.Ordinal);
66
67                 [ThreadStatic]
68                 static KeyedHashAlgorithm hashAlg;
69                 static bool canReuseHashAlg = true;
70
71                 static KeyedHashAlgorithm ReusableHashAlgorithm {
72                         get {
73                                 if (!canReuseHashAlg)
74                                         return null;
75
76                                 if (hashAlg == null) {                          
77                                         MachineKeySection mks = MachineKeySection.Config;
78                                         hashAlg = MachineKeySectionUtils.GetValidationAlgorithm (mks);
79                                         if (!hashAlg.CanReuseTransform) {
80                                                 canReuseHashAlg = false;
81                                                 hashAlg = null;
82                                                 return null;
83                                         }
84                                         hashAlg.Key = MachineKeySectionUtils.GetValidationKey (mks);
85                                 }
86
87                                 if (hashAlg != null)
88                                         hashAlg.Initialize ();
89
90                                 return hashAlg;
91                         }
92                 }
93                 
94                 static string GetStringHash (KeyedHashAlgorithm kha, string str)
95                 {
96                         if (String.IsNullOrEmpty (str))
97                                 return String.Empty;
98
99                         string result;
100                         try {
101                                 _stringHashCacheLock.EnterUpgradeableReadLock ();
102                                 if (stringHashCache.TryGetValue (str, out result))
103                                         return result;
104
105                                 try {
106                                         _stringHashCacheLock.EnterWriteLock ();
107                                         if (stringHashCache.TryGetValue (str, out result))
108                                                 return result;
109                                         
110                                         result = Convert.ToBase64String (kha.ComputeHash (Encoding.UTF8.GetBytes (str)));
111                                         stringHashCache.Add (str, result);
112                                 } finally {
113                                         _stringHashCacheLock.ExitWriteLock ();
114                                 }
115                         } finally {
116                                 _stringHashCacheLock.ExitUpgradeableReadLock ();
117                         }
118                         
119                         return result;
120                 }
121                 
122                 static void InitEmbeddedResourcesUrls (KeyedHashAlgorithm kha, Assembly assembly, string assemblyName, string assemblyHash, AssemblyEmbeddedResources entry)
123                 {
124                         WebResourceAttribute [] attrs = (WebResourceAttribute []) assembly.GetCustomAttributes (typeof (WebResourceAttribute), false);
125                         WebResourceAttribute attr;
126                         string apath = assembly.Location;
127                         for (int i = 0; i < attrs.Length; i++) {
128                                 attr = attrs [i];
129                                 string resourceName = attr.WebResource;
130                                 if (!String.IsNullOrEmpty (resourceName)) {
131                                         string resourceNameHash = GetStringHash (kha, resourceName);
132 #if SYSTEM_WEB_EXTENSIONS
133                                         bool debug = resourceName.EndsWith (".debug.js", StringComparison.OrdinalIgnoreCase);
134                                         string dbgTail = debug ? "d" : String.Empty;
135                                         string rkNoNotify = resourceNameHash + "f" + dbgTail;
136                                         string rkNotify = resourceNameHash + "t" + dbgTail;
137
138                                         if (!entry.Resources.ContainsKey (rkNoNotify)) {
139                                                 var er = new EmbeddedResource () {
140                                                         Name = resourceName,
141                                                         Attribute = attr, 
142                                                         Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, rkNoNotify, debug, false, true)
143                                                 };
144                                                 
145                                                 entry.Resources.Add (rkNoNotify, er);
146                                         }
147                                         
148                                         if (!entry.Resources.ContainsKey (rkNotify)) {
149                                                 var er = new EmbeddedResource () {
150                                                         Name = resourceName,
151                                                         Attribute = attr, 
152                                                         Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, rkNotify, debug, true, true)
153                                                 };
154                                                 
155                                                 entry.Resources.Add (rkNotify, er);
156                                         }
157 #else
158                                         if (!entry.Resources.ContainsKey (resourceNameHash)) {
159                                                 var er = new EmbeddedResource () {
160                                                         Name = resourceName,
161                                                         Attribute = attr, 
162                                                         Url = CreateResourceUrl (kha, assemblyName, assemblyHash, apath, resourceNameHash, false, false, true)
163                                                 };
164                                                 entry.Resources.Add (resourceNameHash, er);
165                                         }
166 #endif
167                                 }
168                         }
169                 }
170
171 #if !SYSTEM_WEB_EXTENSIONS
172                 internal static string GetResourceUrl (Type type, string resourceName)
173                 {
174                         return GetResourceUrl (type.Assembly, resourceName, false);
175                 }
176 #endif
177                 static EmbeddedResource DecryptAssemblyResource (string val, out AssemblyEmbeddedResources entry)
178                 {
179                         entry = null;
180                         
181                         string[] parts = val.Split ('_');
182                         if (parts.Length != 3)
183                                 return null;
184
185                         string asmNameHash = parts [0];
186                         string resNameHash = parts [1];
187                         try {
188                                 _embeddedResourcesLock.EnterReadLock ();
189                                 if (!_embeddedResources.TryGetValue (asmNameHash, out entry) || entry == null)
190                                         return null;
191                                 
192                                 EmbeddedResource res;
193                                 if (!entry.Resources.TryGetValue (resNameHash, out res) || res == null) {
194 #if SYSTEM_WEB_EXTENSIONS
195                                         bool debug = parts [2] == "t";
196                                         if (!debug)
197                                                 return null;
198
199                                         if (!entry.Resources.TryGetValue (resNameHash.Substring (0, resNameHash.Length - 1), out res))
200                                                 return null;
201 #else
202                                         return null;
203 #endif
204                                 }
205                                 
206                                 return res;
207                         } finally {
208                                 _embeddedResourcesLock.ExitReadLock ();
209                         }
210                 }
211
212                 static void GetAssemblyNameAndHashes (KeyedHashAlgorithm kha, Assembly assembly, string resourceName, out string assemblyName, out string assemblyNameHash, out string resourceNameHash)
213                 {
214                         assemblyName = assembly == currAsm ? "s" : assembly.GetName ().FullName;
215                         assemblyNameHash = GetStringHash (kha, assemblyName);
216                         resourceNameHash = GetStringHash (kha, resourceName);
217                 }
218                 
219                 // MUST be called with the _embeddedResourcesLock taken in the upgradeable read lock mode
220                 static AssemblyEmbeddedResources GetAssemblyEmbeddedResource (KeyedHashAlgorithm kha, Assembly assembly, string assemblyNameHash, string assemblyName)
221                 {
222                         AssemblyEmbeddedResources entry;
223                         
224                         if (!_embeddedResources.TryGetValue (assemblyNameHash, out entry) || entry == null) {
225                                 try {
226                                         _embeddedResourcesLock.EnterWriteLock ();
227                                         entry = new AssemblyEmbeddedResources () {
228                                                         AssemblyName = assemblyName
229                                                                 };
230                                         InitEmbeddedResourcesUrls (kha, assembly, assemblyName, assemblyNameHash, entry);
231                                         _embeddedResources.Add (assemblyNameHash, entry);
232                                 } finally {
233                                         _embeddedResourcesLock.ExitWriteLock ();
234                                 }
235                         }
236
237                         return entry;
238                 }
239                 
240                 internal static string GetResourceUrl (Assembly assembly, string resourceName, bool notifyScriptLoaded)
241                 {
242                         if (assembly == null)
243                                 return String.Empty;
244
245                         KeyedHashAlgorithm kha = ReusableHashAlgorithm;
246                         if (kha != null) {
247                                 return GetResourceUrl (kha, assembly, resourceName, notifyScriptLoaded);
248                         } else {
249                                 MachineKeySection mks = MachineKeySection.Config;
250                                 using (kha = MachineKeySectionUtils.GetValidationAlgorithm (mks)) {
251                                         kha.Key = MachineKeySectionUtils.GetValidationKey (mks);
252                                         return GetResourceUrl (kha, assembly, resourceName, notifyScriptLoaded);
253                                 }
254                         }
255                 }
256
257                 static string GetResourceUrl (KeyedHashAlgorithm kha, Assembly assembly, string resourceName, bool notifyScriptLoaded)
258                 {
259                         string assemblyName;
260                         string assemblyNameHash;
261                         string resourceNameHash;
262
263                         GetAssemblyNameAndHashes (kha, assembly, resourceName, out assemblyName, out assemblyNameHash, out resourceNameHash);
264                         bool debug = false;
265                         string url;
266                         AssemblyEmbeddedResources entry;
267                         bool includeTimeStamp = true;
268
269                         try {
270                                 _embeddedResourcesLock.EnterUpgradeableReadLock ();
271                                 entry = GetAssemblyEmbeddedResource (kha, assembly, assemblyNameHash, assemblyName);
272                                 string lookupKey;
273 #if SYSTEM_WEB_EXTENSIONS
274                                 debug = resourceName.EndsWith (".debug.js", StringComparison.OrdinalIgnoreCase);
275                                 string dbgTail = debug ? "d" : String.Empty;
276                                 lookupKey = resourceNameHash + (notifyScriptLoaded ? "t" : "f") + dbgTail;
277                                 CheckIfResourceIsCompositeScript (resourceName, ref includeTimeStamp);
278 #else
279                                 lookupKey = resourceNameHash;
280 #endif
281                                 EmbeddedResource res;
282                                 if (entry.Resources.TryGetValue (lookupKey, out res) && res != null)
283                                         url = res.Url;
284                                 else {
285 #if SYSTEM_WEB_EXTENSIONS
286                                         if (debug) {
287                                                 resourceNameHash = GetStringHash (kha, resourceName.Substring (0, resourceName.Length - 9) + ".js");
288                                                 lookupKey = resourceNameHash + (notifyScriptLoaded ? "t" : "f");
289                                         
290                                                 if (entry.Resources.TryGetValue (lookupKey, out res) && res != null)
291                                                         url = res.Url;
292                                                 else
293                                                         url = null;
294                                         } else
295 #endif
296                                                 url = null;
297                                 }
298                         } finally {
299                                 _embeddedResourcesLock.ExitUpgradeableReadLock ();
300                         }
301
302                         if (url == null)
303                                 url = CreateResourceUrl (kha, assemblyName, assemblyNameHash, assembly.Location, resourceNameHash, debug, notifyScriptLoaded, includeTimeStamp);
304                         
305                         return url;
306                 }
307                 
308                 static string CreateResourceUrl (KeyedHashAlgorithm kha, string assemblyName, string assemblyNameHash, string assemblyPath, string resourceNameHash, bool debug,
309                                                  bool notifyScriptLoaded, bool includeTimeStamp)
310                 {
311                         string atime = String.Empty;
312                         string extra = String.Empty;
313 #if SYSTEM_WEB_EXTENSIONS
314                         extra = QueryParamSeparator + "n=" + (notifyScriptLoaded ? "t" : "f");
315 #endif
316
317                         if (includeTimeStamp) {
318                                 if (!String.IsNullOrEmpty (assemblyPath) && File.Exists (assemblyPath))
319                                         atime = QueryParamSeparator + "t=" + File.GetLastWriteTimeUtc (assemblyPath).Ticks;
320                                 else
321                                         atime = QueryParamSeparator + "t=" + DateTime.UtcNow.Ticks;
322                         }
323                         string d = HttpUtility.UrlEncode (assemblyNameHash + "_" + resourceNameHash +  (debug ? "_t" : "_f"));
324                         string href = HandlerFileName + "?d=" + d + atime + extra;
325                         HttpContext ctx = HttpContext.Current;
326                         HttpRequest req = ctx != null ? ctx.Request : null;
327                         if (req != null) {
328                                 string appPath = VirtualPathUtility.AppendTrailingSlash (req.ApplicationPath);
329                                 href = appPath + href;
330                         }
331                         
332                         return href;
333                 }
334
335                 bool HasIfModifiedSince (HttpRequest request, out DateTime modified)
336                 {
337                         string modif_since = request.Headers ["If-Modified-Since"];
338                         if (String.IsNullOrEmpty (modif_since)) {
339                                 modified = DateTime.MinValue;
340                                 return false;
341                         }
342
343                         try {
344                                 if (DateTime.TryParseExact (modif_since, "r", null, 0, out modified))
345                                         return true;
346                         } catch {
347                                 modified = DateTime.MinValue;
348                         }
349
350                         return false;
351                 }
352
353                 void RespondWithNotModified (HttpContext context)
354                 {
355                         HttpResponse response = context.Response;
356                         response.Clear ();
357                         response.StatusCode = 304;
358                         response.ContentType = null;
359                         context.ApplicationInstance.CompleteRequest ();
360                 }
361                 
362                 void SendEmbeddedResource (HttpContext context, out EmbeddedResource res, out Assembly assembly)
363                 {
364                         HttpRequest request = context.Request;
365                         NameValueCollection queryString = request.QueryString;
366                         
367                         // val is URL-encoded, which means every + has been replaced with ' ', we
368                         // need to revert that or the base64 conversion will fail.
369                         string d = queryString ["d"];
370                         if (!String.IsNullOrEmpty (d))
371                                 d = d.Replace (' ', '+');
372
373                         AssemblyEmbeddedResources entry;
374                         res = DecryptAssemblyResource (d, out entry);
375                         WebResourceAttribute wra = res != null ? res.Attribute : null;
376                         if (wra == null)
377                                 throw new HttpException (404, "Resource not found");
378
379                         if (entry.AssemblyName == "s")
380                                 assembly = currAsm;
381                         else
382                                 assembly = Assembly.Load (entry.AssemblyName);
383                         
384                         DateTime modified;
385                         if (HasIfModifiedSince (request, out modified)) {
386                                 if (File.GetLastWriteTimeUtc (assembly.Location) <= modified) {
387                                         RespondWithNotModified (context);
388                                         return;
389                                 }
390                         }
391
392                         HttpResponse response = context.Response;
393                         response.ContentType = wra.ContentType;
394
395                         DateTime utcnow = DateTime.UtcNow;
396                         response.Headers.Add ("Last-Modified", utcnow.ToString ("r"));
397                         response.ExpiresAbsolute = utcnow.AddYears (1);
398                         response.CacheControl = "public";
399
400                         Stream s = assembly.GetManifestResourceStream (res.Name);
401                         if (s == null)
402                                 throw new HttpException (404, "Resource " + res.Name + " not found");
403
404                         if (wra.PerformSubstitution) {
405                                 using (StreamReader r = new StreamReader (s)) {
406                                         TextWriter w = response.Output;
407                                         new PerformSubstitutionHelper (assembly).PerformSubstitution (r, w);
408                                 }
409                         } else if (response.OutputStream is HttpResponseStream) {
410                                 UnmanagedMemoryStream st = (UnmanagedMemoryStream) s;
411                                 HttpResponseStream hstream = (HttpResponseStream) response.OutputStream;
412                                 unsafe {
413                                         hstream.WritePtr (new IntPtr (st.PositionPointer), (int) st.Length);
414                                 }
415                         } else {
416                                 byte [] buf = new byte [1024];
417                                 Stream output = response.OutputStream;
418                                 int c;
419                                 do {
420                                         c = s.Read (buf, 0, 1024);
421                                         output.Write (buf, 0, c);
422                                 } while (c > 0);
423                         }
424                 }
425                 
426 #if !SYSTEM_WEB_EXTENSIONS
427                 void System.Web.IHttpHandler.ProcessRequest (HttpContext context)
428                 {
429                         EmbeddedResource res;
430                         Assembly assembly;
431                         
432                         SendEmbeddedResource (context, out res, out assembly);
433                 }
434 #endif
435                 sealed class PerformSubstitutionHelper
436                 {
437                         readonly Assembly _assembly;
438                         static readonly Regex _regex = new Regex (@"\<%=[ ]*WebResource[ ]*\([ ]*""([^""]+)""[ ]*\)[ ]*%\>");
439
440                         public PerformSubstitutionHelper (Assembly assembly) {
441                                 _assembly = assembly;
442                         }
443
444                         public void PerformSubstitution (TextReader reader, TextWriter writer) {
445                                 string line = reader.ReadLine ();
446                                 while (line != null) {
447                                         if (line.Length > 0 && _regex.IsMatch (line))
448                                                 line = _regex.Replace (line, new MatchEvaluator (PerformSubstitutionReplace));
449                                         writer.WriteLine (line);
450                                         line = reader.ReadLine ();
451                                 }
452                         }
453
454                         string PerformSubstitutionReplace (Match m) {
455                                 string resourceName = m.Groups [1].Value;
456 #if SYSTEM_WEB_EXTENSIONS
457                                 return ScriptResourceHandler.GetResourceUrl (_assembly, resourceName, false);
458 #else
459                                 return AssemblyResourceLoader.GetResourceUrl (_assembly, resourceName, false);
460 #endif
461                         }
462                 }
463                 
464 #if !SYSTEM_WEB_EXTENSIONS
465                 bool System.Web.IHttpHandler.IsReusable { get { return true; } }
466 #endif
467                 sealed class EmbeddedResource
468                 {
469                         public string Name;
470                         public string Url;
471                         public WebResourceAttribute Attribute;
472                 }
473                 
474                 sealed class AssemblyEmbeddedResources
475                 {
476                         public string AssemblyName = String.Empty;
477                         public Dictionary <string, EmbeddedResource> Resources = new Dictionary <string, EmbeddedResource> (StringComparer.Ordinal);
478                 }               
479         }
480 }
481