/BlogEngine/DotNetSlave.BusinessLogic/Web/HttpModules/CompressionModule.cs
C# | 420 lines | 187 code | 50 blank | 183 comment | 20 complexity | 7d6ed6bddfaddc744ad6a0cfed3e9f3f MD5 | raw file
1namespace BlogEngine.Core.Web.HttpModules 2{ 3 using System; 4 using System.IO; 5 using System.IO.Compression; 6 using System.Text; 7 using System.Text.RegularExpressions; 8 using System.Web; 9 using System.Web.UI; 10 11 /// <summary> 12 /// Compresses the output using standard gzip/deflate. 13 /// </summary> 14 public sealed class CompressionModule : IHttpModule 15 { 16 #region Constants and Fields 17 18 /// <summary> 19 /// The deflate string. 20 /// </summary> 21 private const string Deflate = "deflate"; 22 23 /// <summary> 24 /// The gzip string. 25 /// </summary> 26 private const string Gzip = "gzip"; 27 28 #endregion 29 30 #region Public Methods 31 32 /// <summary> 33 /// Compresses the response stream using either deflate or gzip depending on the client. 34 /// </summary> 35 /// <param name="context"> 36 /// The HTTP context to compress. 37 /// </param> 38 public static void CompressResponse(HttpContext context) 39 { 40 if (IsEncodingAccepted(Deflate)) 41 { 42 context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress); 43 WillCompressResponse = true; 44 SetEncoding(Deflate); 45 } 46 else if (IsEncodingAccepted(Gzip)) 47 { 48 context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress); 49 WillCompressResponse = true; 50 SetEncoding(Gzip); 51 } 52 } 53 54 #endregion 55 56 #region Private Methods 57 58 private static bool WillCompressResponse 59 { 60 get 61 { 62 HttpContext context = HttpContext.Current; 63 if (context == null) { return false; } 64 return context.Items["will-compress-resource"] != null && (bool)context.Items["will-compress-resource"]; 65 } 66 set 67 { 68 HttpContext context = HttpContext.Current; 69 if (context == null) { return; } 70 context.Items["will-compress-resource"] = value; 71 } 72 } 73 74 #endregion 75 76 #region Implemented Interfaces 77 78 #region IHttpModule 79 80 /// <summary> 81 /// Disposes of the resources (other than memory) used by the module 82 /// that implements <see cref="T:System.Web.IHttpModule"></see>. 83 /// </summary> 84 void IHttpModule.Dispose() 85 { 86 // Nothing to dispose; 87 } 88 89 /// <summary> 90 /// Initializes a module and prepares it to handle requests. 91 /// </summary> 92 /// <param name="context"> 93 /// An <see cref="T:System.Web.HttpApplication"></see> 94 /// that provides access to the methods, properties, and events common to 95 /// all application objects within an ASP.NET application. 96 /// </param> 97 void IHttpModule.Init(HttpApplication context) 98 { 99 context.PreRequestHandlerExecute += ContextPostReleaseRequestState; 100 context.Error += new EventHandler(context_Error); 101 } 102 103 void context_Error(object sender, EventArgs e) 104 { 105 HttpContext context = ((HttpApplication)sender).Context; 106 Exception ex = context.Server.GetLastError(); 107 108 // If this CompressionModule will be compressing the response and an unhandled exception 109 // has occurred, remove the WebResourceFilter as that will cause garbage characters to 110 // be sent to the browser instead of a yellow screen of death. 111 if (WillCompressResponse) 112 { 113 context.Response.Filter = null; 114 WillCompressResponse = false; 115 } 116 } 117 118 #endregion 119 120 #endregion 121 122 #region Methods 123 124 /// <summary> 125 /// Checks the request headers to see if the specified 126 /// encoding is accepted by the client. 127 /// </summary> 128 /// <param name="encoding"> 129 /// The encoding. 130 /// </param> 131 /// <returns> 132 /// The is encoding accepted. 133 /// </returns> 134 private static bool IsEncodingAccepted(string encoding) 135 { 136 var context = HttpContext.Current; 137 return context.Request.Headers["Accept-encoding"] != null && 138 context.Request.Headers["Accept-encoding"].Contains(encoding); 139 } 140 141 /// <summary> 142 /// Adds the specified encoding to the response headers. 143 /// </summary> 144 /// <param name="encoding">The encoding.</param> 145 private static void SetEncoding(string encoding) 146 { 147 HttpContext.Current.Response.AppendHeader("Content-encoding", encoding); 148 } 149 150 /// <summary> 151 /// Handles the BeginRequest event of the context control. 152 /// </summary> 153 /// <param name="sender"> 154 /// The source of the event. 155 /// </param> 156 /// <param name="e"> 157 /// The <see cref="System.EventArgs"/> instance containing the event data. 158 /// </param> 159 private static void ContextPostReleaseRequestState(object sender, EventArgs e) 160 { 161 var context = ((HttpApplication)sender).Context; 162 if (!BlogSettings.Instance.EnableHttpCompression) { return; } 163 164 if (context.CurrentHandler is Page && context.Request["HTTP_X_MICROSOFTAJAX"] == null && 165 context.Request.HttpMethod == "GET") 166 { 167 CompressResponse(context); 168 169 if (BlogSettings.Instance.CompressWebResource) 170 { 171 context.Response.Filter = new WebResourceFilter(context.Response.Filter); 172 WillCompressResponse = true; 173 } 174 } 175 else if (!BlogSettings.Instance.CompressWebResource && context.Request.Path.Contains("WebResource.axd")) 176 { 177 context.Response.Cache.SetExpires(DateTime.Now.AddDays(30)); 178 } 179 } 180 181 #endregion 182 183 /// <summary> 184 /// The web resource filter. 185 /// </summary> 186 private class WebResourceFilter : Stream 187 { 188 #region Constants and Fields 189 190 /// <summary> 191 /// The _sink. 192 /// </summary> 193 private readonly Stream sink; 194 195 /// <summary> 196 /// Regex for parsing webresource.axd 197 /// </summary> 198 private static readonly Regex WebResourceRegex = 199 new Regex( 200 "<script\\s*src=\"((?=[^\"]*webresource.axd)[^\"]*)\"\\s*type=\"text/javascript\"[^>]*>[^<]*(?:</script>)?", 201 RegexOptions.IgnoreCase | RegexOptions.Compiled); 202 203 #endregion 204 205 #region Constructors and Destructors 206 207 /// <summary> 208 /// Initializes a new instance of the <see cref="WebResourceFilter"/> class. 209 /// </summary> 210 /// <param name="sink"> 211 /// The sink stream. 212 /// </param> 213 public WebResourceFilter(Stream sink) 214 { 215 this.sink = sink; 216 } 217 218 #endregion 219 220 #region Properties 221 222 /// <summary> 223 /// Gets a value indicating whether CanRead. 224 /// </summary> 225 public override bool CanRead 226 { 227 get 228 { 229 return true; 230 } 231 } 232 233 /// <summary> 234 /// Gets a value indicating whether CanSeek. 235 /// </summary> 236 public override bool CanSeek 237 { 238 get 239 { 240 return true; 241 } 242 } 243 244 /// <summary> 245 /// Gets a value indicating whether CanWrite. 246 /// </summary> 247 public override bool CanWrite 248 { 249 get 250 { 251 return true; 252 } 253 } 254 255 /// <summary> 256 /// Gets Length. 257 /// </summary> 258 public override long Length 259 { 260 get 261 { 262 return 0; 263 } 264 } 265 266 /// <summary> 267 /// Gets or sets Position. 268 /// </summary> 269 public override long Position { get; set; } 270 271 #endregion 272 273 #region Public Methods 274 275 /// <summary> 276 /// The close. 277 /// </summary> 278 public override void Close() 279 { 280 this.sink.Close(); 281 } 282 283 /// <summary> 284 /// Evaluates the replacement for each link match. 285 /// </summary> 286 /// <param name="match"> 287 /// The match. 288 /// </param> 289 /// <returns> 290 /// The evaluator. 291 /// </returns> 292 public string Evaluator(Match match) 293 { 294 var relative = match.Groups[1].Value; 295 var absolute = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority); 296 return match.Value.Replace( 297 relative, string.Format("{0}js.axd?path={1}", Utils.ApplicationRelativeWebRoot, HttpUtility.UrlEncode(absolute + relative))); 298 } 299 300 /// <summary> 301 /// When overridden in a derived class, clears all buffers for this stream and causes any buffered data to be written to the underlying device. 302 /// </summary> 303 /// <exception cref="T:System.IO.IOException"> 304 /// An I/O error occurs. 305 /// </exception> 306 public override void Flush() 307 { 308 this.sink.Flush(); 309 } 310 311 /// <summary> 312 /// When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. 313 /// </summary> 314 /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param> 315 /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param> 316 /// <param name="count">The maximum number of bytes to be read from the current stream.</param> 317 /// <returns> 318 /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. 319 /// </returns> 320 /// <exception cref="T:System.ArgumentException"> 321 /// The sum of <paramref name="offset"/> and <paramref name="count"/> is larger than the buffer length. 322 /// </exception> 323 /// <exception cref="T:System.ArgumentNullException"> 324 /// <paramref name="buffer"/> is null. 325 /// </exception> 326 /// <exception cref="T:System.ArgumentOutOfRangeException"> 327 /// <paramref name="offset"/> or <paramref name="count"/> is negative. 328 /// </exception> 329 /// <exception cref="T:System.IO.IOException"> 330 /// An I/O error occurs. 331 /// </exception> 332 /// <exception cref="T:System.NotSupportedException"> 333 /// The stream does not support reading. 334 /// </exception> 335 /// <exception cref="T:System.ObjectDisposedException"> 336 /// Methods were called after the stream was closed. 337 /// </exception> 338 public override int Read(byte[] buffer, int offset, int count) 339 { 340 return this.sink.Read(buffer, offset, count); 341 } 342 343 /// <summary> 344 /// When overridden in a derived class, sets the position within the current stream. 345 /// </summary> 346 /// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param> 347 /// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.</param> 348 /// <returns> 349 /// The new position within the current stream. 350 /// </returns> 351 /// <exception cref="T:System.IO.IOException"> 352 /// An I/O error occurs. 353 /// </exception> 354 /// <exception cref="T:System.NotSupportedException"> 355 /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. 356 /// </exception> 357 /// <exception cref="T:System.ObjectDisposedException"> 358 /// Methods were called after the stream was closed. 359 /// </exception> 360 public override long Seek(long offset, SeekOrigin origin) 361 { 362 return this.sink.Seek(offset, origin); 363 } 364 365 /// <summary> 366 /// When overridden in a derived class, sets the length of the current stream. 367 /// </summary> 368 /// <param name="value">The desired length of the current stream in bytes.</param> 369 /// <exception cref="T:System.IO.IOException"> 370 /// An I/O error occurs. 371 /// </exception> 372 /// <exception cref="T:System.NotSupportedException"> 373 /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. 374 /// </exception> 375 /// <exception cref="T:System.ObjectDisposedException"> 376 /// Methods were called after the stream was closed. 377 /// </exception> 378 public override void SetLength(long value) 379 { 380 this.sink.SetLength(value); 381 } 382 383 /// <summary> 384 /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. 385 /// </summary> 386 /// <param name="buffer">An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.</param> 387 /// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.</param> 388 /// <param name="count">The number of bytes to be written to the current stream.</param> 389 /// <exception cref="T:System.ArgumentException"> 390 /// The sum of <paramref name="offset"/> and <paramref name="count"/> is greater than the buffer length. 391 /// </exception> 392 /// <exception cref="T:System.ArgumentNullException"> 393 /// <paramref name="buffer"/> is null. 394 /// </exception> 395 /// <exception cref="T:System.ArgumentOutOfRangeException"> 396 /// <paramref name="offset"/> or <paramref name="count"/> is negative. 397 /// </exception> 398 /// <exception cref="T:System.IO.IOException"> 399 /// An I/O error occurs. 400 /// </exception> 401 /// <exception cref="T:System.NotSupportedException"> 402 /// The stream does not support writing. 403 /// </exception> 404 /// <exception cref="T:System.ObjectDisposedException"> 405 /// Methods were called after the stream was closed. 406 /// </exception> 407 public override void Write(byte[] buffer, int offset, int count) 408 { 409 var html = Encoding.UTF8.GetString(buffer, offset, count); 410 411 html = WebResourceRegex.Replace(html, new MatchEvaluator(this.Evaluator)); 412 413 var outdata = Encoding.UTF8.GetBytes(html); 414 this.sink.Write(outdata, 0, outdata.GetLength(0)); 415 } 416 417 #endregion 418 } 419 } 420}