PageRenderTime 17ms CodeModel.GetById 2ms app.highlight 9ms RepoModel.GetById 2ms app.codeStats 0ms

/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}