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
.Collections
.Specialized
;
40 using System
.Security
.Cryptography
;
42 using System
.Text
.RegularExpressions
;
43 using System
.Threading
;
44 using System
.Web
.Configuration
;
45 using System
.Web
.Util
;
47 namespace System
.Web
.Handlers
49 #if SYSTEM_WEB_EXTENSIONS
50 partial class ScriptResourceHandler
52 const string HandlerFileName
= "ScriptResource.axd";
53 static Assembly currAsm
= typeof (ScriptResourceHandler
).Assembly
;
55 public sealed class AssemblyResourceLoader
: IHttpHandler
57 const string HandlerFileName
= "WebResource.axd";
58 static Assembly currAsm
= typeof (AssemblyResourceLoader
).Assembly
;
60 const char QueryParamSeparator
= '&';
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
);
68 static KeyedHashAlgorithm hashAlg
;
69 static bool canReuseHashAlg
= true;
71 static KeyedHashAlgorithm ReusableHashAlgorithm
{
76 if (hashAlg
== null) {
77 MachineKeySection mks
= MachineKeySection
.Config
;
78 hashAlg
= MachineKeySectionUtils
.GetValidationAlgorithm (mks
);
79 if (!hashAlg
.CanReuseTransform
) {
80 canReuseHashAlg
= false;
84 hashAlg
.Key
= MachineKeySectionUtils
.GetValidationKey (mks
);
88 hashAlg
.Initialize ();
94 static string GetStringHash (KeyedHashAlgorithm kha
, string str
)
96 if (String
.IsNullOrEmpty (str
))
101 _stringHashCacheLock
.EnterUpgradeableReadLock ();
102 if (stringHashCache
.TryGetValue (str
, out result
))
106 _stringHashCacheLock
.EnterWriteLock ();
107 if (stringHashCache
.TryGetValue (str
, out result
))
110 result
= Convert
.ToBase64String (kha
.ComputeHash (Encoding
.UTF8
.GetBytes (str
)));
111 stringHashCache
.Add (str
, result
);
113 _stringHashCacheLock
.ExitWriteLock ();
116 _stringHashCacheLock
.ExitUpgradeableReadLock ();
122 static void InitEmbeddedResourcesUrls (KeyedHashAlgorithm kha
, Assembly assembly
, string assemblyName
, string assemblyHash
, AssemblyEmbeddedResources entry
)
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
++) {
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
;
138 if (!entry
.Resources
.ContainsKey (rkNoNotify
)) {
139 var er
= new EmbeddedResource () {
142 Url
= CreateResourceUrl (kha
, assemblyName
, assemblyHash
, apath
, rkNoNotify
, debug
, false, true)
145 entry
.Resources
.Add (rkNoNotify
, er
);
148 if (!entry
.Resources
.ContainsKey (rkNotify
)) {
149 var er
= new EmbeddedResource () {
152 Url
= CreateResourceUrl (kha
, assemblyName
, assemblyHash
, apath
, rkNotify
, debug
, true, true)
155 entry
.Resources
.Add (rkNotify
, er
);
158 if (!entry
.Resources
.ContainsKey (resourceNameHash
)) {
159 var er
= new EmbeddedResource () {
162 Url
= CreateResourceUrl (kha
, assemblyName
, assemblyHash
, apath
, resourceNameHash
, false, false, true)
164 entry
.Resources
.Add (resourceNameHash
, er
);
171 #if !SYSTEM_WEB_EXTENSIONS
172 internal static string GetResourceUrl (Type type
, string resourceName
)
174 return GetResourceUrl (type
.Assembly
, resourceName
, false);
177 static EmbeddedResource
DecryptAssemblyResource (string val
, out AssemblyEmbeddedResources entry
)
181 string[] parts
= val
.Split ('_');
182 if (parts
.Length
!= 3)
185 string asmNameHash
= parts
[0];
186 string resNameHash
= parts
[1];
188 _embeddedResourcesLock
.EnterReadLock ();
189 if (!_embeddedResources
.TryGetValue (asmNameHash
, out entry
) || entry
== null)
192 EmbeddedResource res
;
193 if (!entry
.Resources
.TryGetValue (resNameHash
, out res
) || res
== null) {
194 #if SYSTEM_WEB_EXTENSIONS
195 bool debug
= parts
[2] == "t";
199 if (!entry
.Resources
.TryGetValue (resNameHash
.Substring (0, resNameHash
.Length
- 1), out res
))
208 _embeddedResourcesLock
.ExitReadLock ();
212 static void GetAssemblyNameAndHashes (KeyedHashAlgorithm kha
, Assembly assembly
, string resourceName
, out string assemblyName
, out string assemblyNameHash
, out string resourceNameHash
)
214 assemblyName
= assembly
== currAsm
? "s" : assembly
.GetName ().FullName
;
215 assemblyNameHash
= GetStringHash (kha
, assemblyName
);
216 resourceNameHash
= GetStringHash (kha
, resourceName
);
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
)
222 AssemblyEmbeddedResources entry
;
224 if (!_embeddedResources
.TryGetValue (assemblyNameHash
, out entry
) || entry
== null) {
226 _embeddedResourcesLock
.EnterWriteLock ();
227 entry
= new AssemblyEmbeddedResources () {
228 AssemblyName
= assemblyName
230 InitEmbeddedResourcesUrls (kha
, assembly
, assemblyName
, assemblyNameHash
, entry
);
231 _embeddedResources
.Add (assemblyNameHash
, entry
);
233 _embeddedResourcesLock
.ExitWriteLock ();
240 internal static string GetResourceUrl (Assembly assembly
, string resourceName
, bool notifyScriptLoaded
)
242 if (assembly
== null)
245 KeyedHashAlgorithm kha
= ReusableHashAlgorithm
;
247 return GetResourceUrl (kha
, assembly
, resourceName
, notifyScriptLoaded
);
249 MachineKeySection mks
= MachineKeySection
.Config
;
250 using (kha
= MachineKeySectionUtils
.GetValidationAlgorithm (mks
)) {
251 kha
.Key
= MachineKeySectionUtils
.GetValidationKey (mks
);
252 return GetResourceUrl (kha
, assembly
, resourceName
, notifyScriptLoaded
);
257 static string GetResourceUrl (KeyedHashAlgorithm kha
, Assembly assembly
, string resourceName
, bool notifyScriptLoaded
)
260 string assemblyNameHash
;
261 string resourceNameHash
;
263 GetAssemblyNameAndHashes (kha
, assembly
, resourceName
, out assemblyName
, out assemblyNameHash
, out resourceNameHash
);
266 AssemblyEmbeddedResources entry
;
267 bool includeTimeStamp
= true;
270 _embeddedResourcesLock
.EnterUpgradeableReadLock ();
271 entry
= GetAssemblyEmbeddedResource (kha
, assembly
, assemblyNameHash
, assemblyName
);
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
);
279 lookupKey
= resourceNameHash
;
281 EmbeddedResource res
;
282 if (entry
.Resources
.TryGetValue (lookupKey
, out res
) && res
!= null)
285 #if SYSTEM_WEB_EXTENSIONS
287 resourceNameHash
= GetStringHash (kha
, resourceName
.Substring (0, resourceName
.Length
- 9) + ".js");
288 lookupKey
= resourceNameHash
+ (notifyScriptLoaded
? "t" : "f");
290 if (entry
.Resources
.TryGetValue (lookupKey
, out res
) && res
!= null)
299 _embeddedResourcesLock
.ExitUpgradeableReadLock ();
303 url
= CreateResourceUrl (kha
, assemblyName
, assemblyNameHash
, assembly
.Location
, resourceNameHash
, debug
, notifyScriptLoaded
, includeTimeStamp
);
308 static string CreateResourceUrl (KeyedHashAlgorithm kha
, string assemblyName
, string assemblyNameHash
, string assemblyPath
, string resourceNameHash
, bool debug
,
309 bool notifyScriptLoaded
, bool includeTimeStamp
)
311 string atime
= String
.Empty
;
312 string extra
= String
.Empty
;
313 #if SYSTEM_WEB_EXTENSIONS
314 extra
= QueryParamSeparator
+ "n=" + (notifyScriptLoaded
? "t" : "f");
317 if (includeTimeStamp
) {
318 if (!String
.IsNullOrEmpty (assemblyPath
) && File
.Exists (assemblyPath
))
319 atime
= QueryParamSeparator
+ "t=" + File
.GetLastWriteTimeUtc (assemblyPath
).Ticks
;
321 atime
= QueryParamSeparator
+ "t=" + DateTime
.UtcNow
.Ticks
;
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;
328 string appPath
= VirtualPathUtility
.AppendTrailingSlash (req
.ApplicationPath
);
329 href
= appPath
+ href
;
335 bool HasIfModifiedSince (HttpRequest request
, out DateTime modified
)
337 string modif_since
= request
.Headers
["If-Modified-Since"];
338 if (String
.IsNullOrEmpty (modif_since
)) {
339 modified
= DateTime
.MinValue
;
344 if (DateTime
.TryParseExact (modif_since
, "r", null, 0, out modified
))
347 modified
= DateTime
.MinValue
;
353 void RespondWithNotModified (HttpContext context
)
355 HttpResponse response
= context
.Response
;
357 response
.StatusCode
= 304;
358 response
.ContentType
= null;
359 context
.ApplicationInstance
.CompleteRequest ();
362 void SendEmbeddedResource (HttpContext context
, out EmbeddedResource res
, out Assembly assembly
)
364 HttpRequest request
= context
.Request
;
365 NameValueCollection queryString
= request
.QueryString
;
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 (' ', '+');
373 AssemblyEmbeddedResources entry
;
374 res
= DecryptAssemblyResource (d
, out entry
);
375 WebResourceAttribute wra
= res
!= null ? res
.Attribute
: null;
377 throw new HttpException (404, "Resource not found");
379 if (entry
.AssemblyName
== "s")
382 assembly
= Assembly
.Load (entry
.AssemblyName
);
385 if (HasIfModifiedSince (request
, out modified
)) {
386 if (File
.GetLastWriteTimeUtc (assembly
.Location
) <= modified
) {
387 RespondWithNotModified (context
);
392 HttpResponse response
= context
.Response
;
393 response
.ContentType
= wra
.ContentType
;
395 DateTime utcnow
= DateTime
.UtcNow
;
396 response
.Headers
.Add ("Last-Modified", utcnow
.ToString ("r"));
397 response
.ExpiresAbsolute
= utcnow
.AddYears (1);
398 response
.CacheControl
= "public";
400 Stream s
= assembly
.GetManifestResourceStream (res
.Name
);
402 throw new HttpException (404, "Resource " + res
.Name
+ " not found");
404 if (wra
.PerformSubstitution
) {
405 using (StreamReader r
= new StreamReader (s
)) {
406 TextWriter w
= response
.Output
;
407 new PerformSubstitutionHelper (assembly
).PerformSubstitution (r
, w
);
409 } else if (response
.OutputStream
is HttpResponseStream
) {
410 UnmanagedMemoryStream st
= (UnmanagedMemoryStream
) s
;
411 HttpResponseStream hstream
= (HttpResponseStream
) response
.OutputStream
;
413 hstream
.WritePtr (new IntPtr (st
.PositionPointer
), (int) st
.Length
);
416 byte [] buf
= new byte [1024];
417 Stream output
= response
.OutputStream
;
420 c
= s
.Read (buf
, 0, 1024);
421 output
.Write (buf
, 0, c
);
426 #if !SYSTEM_WEB_EXTENSIONS
427 void System
.Web
.IHttpHandler
.ProcessRequest (HttpContext context
)
429 EmbeddedResource res
;
432 SendEmbeddedResource (context
, out res
, out assembly
);
435 sealed class PerformSubstitutionHelper
437 readonly Assembly _assembly
;
438 static readonly Regex _regex
= new Regex (@"\<%=[ ]*WebResource[ ]*\([ ]*""([^""]+)""[ ]*\)[ ]*%\>");
440 public PerformSubstitutionHelper (Assembly assembly
) {
441 _assembly
= assembly
;
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 ();
454 string PerformSubstitutionReplace (Match m
) {
455 string resourceName
= m
.Groups
[1].Value
;
456 #if SYSTEM_WEB_EXTENSIONS
457 return ScriptResourceHandler
.GetResourceUrl (_assembly
, resourceName
, false);
459 return AssemblyResourceLoader
.GetResourceUrl (_assembly
, resourceName
, false);
464 #if !SYSTEM_WEB_EXTENSIONS
465 bool System
.Web
.IHttpHandler
.IsReusable { get { return true; }
}
467 sealed class EmbeddedResource
471 public WebResourceAttribute Attribute
;
474 sealed class AssemblyEmbeddedResources
476 public string AssemblyName
= String
.Empty
;
477 public Dictionary
<string, EmbeddedResource
> Resources
= new Dictionary
<string, EmbeddedResource
> (StringComparer
.Ordinal
);