Click here to Skip to main content
15,883,901 members
Articles / Web Development / ASP.NET

HTTP Compression Module

Rate me:
Please Sign up or sign in to vote.
4.81/5 (39 votes)
19 Mar 2008CPOL6 min read 365.6K   4.8K   153  
A compression module for ASP.NET that works with WebResource.axd, JavaScript, and CSS
/// 
/// As a starting point for this module i used the Blowery module written by Ben Lowery.
/// his blog is located at : http://www.blowery.org/blog/
/// After studying the module that he wrote I couldn't see the point of all the inherited streams
/// because the one we use is already limited with a bunch of constraints.
/// http://www.blowery.org/code/HttpCompressionModule.html
/// The version he has for .net 2.0 is ported from .net 1.1 and the configuration properties don't follow 
/// the new schema anymore. 
/// Because of the change from #ziplib to the native .net 2.0 everything that has to do with quality 
/// doesn't count anymore.
/// I decided to rewrite his library to a .net 2.0 version of it which turned out to be much shorter.
/// So this module is copyrighted by Ivan Porto Carrero, Flanders International Marketing Ltd. 2006
/// Blog : http://www.flanders.co.nz/blog
/// You are free to use this module as long as you keep this notice in the source files
/// 
/// 23/01/2006
/// I changed the module to do work at the post release request state event in stead of at begin request.
/// When fired with begin request the parameters get zipped and form values get affected which kills the correct processing of the page.
/// The compression now takes place after the entire content has been created.
/// 
/// The above was written by Flanders.
/// The Items I added are marked with DC.
/// 
/// A quick explanation of the web resource compression.  A call to WebResource.axd using
/// the module breaks it for some reason.  I tried a lot of different things to get around this
/// but to no avail.  Finally I came up with the following solution.  Hook into the begin request
/// and check if it is a webResource.axd.  If it is then call CompleteRequest so it goes straight
/// to the EndRequest method.  This is so no processing is done on the request.  In EndRequest,
/// send it to the CompressWebResource object.  This then checks to see if there is an Etag (cached on browser),
/// if there is it sends a Not Modified header. (The webresource is loaded every time normally, this adds
/// browser cache ability to WebResource files.)  Then I check if a file is cached, if it is then I 
/// send the cached file.  Otherwise, I create a HttpWebRequest and request the WebResource.axd, but add a 'u=1'
/// to the query string so I know to let it pass through the above and retrieves the WebResource.
/// Then I check the content-type, if it is neither javascript or css then I send it to the 
/// browser, first adding an Etag to allow browser caching.  If it is javascript or css,
/// then get the allowed encoding and encode the data and cache it on disk.
/// 
/// 
/// I expect the credit to be given where credit is due.  The module is created by Flanders
/// and the above copyright applies.  The Compress.cs,CompressHandler.cs,and CompressWebResource.cs
/// were created by me, Darick Carpenter.  The CompressHandler is a port from PHP obtained here:
/// http://rakaz.nl/item/make_your_pages_load_faster_by_combining_and_compressing_javascript_and_css_files
/// and the copyright from that port is on CompressHandler.cs.  The CompressWebResource was
/// created by me and I echo the copyright notice in CompressHandler.cs offering no warranty.  
/// 
/// Darick Carpenter
///
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.IO;
using System.IO.Compression;
using System.Configuration;

namespace DC.Web.HttpCompress
{
    /// <summary>
    /// The Http Module that will compress the outputstream to the browser if it is supported by the browser.
    /// </summary>
    public class HttpModule : IHttpModule
    {
        #region IHttpModule Members

        public void Dispose()
        {

        }

        public void Init(HttpApplication context)
        {
            /* The Post Release Request State is the event most fitted for the task of adding a filter
            // Everything else is too soon or too late. At this point in the execution phase the entire 
             * response content is created and the page has fully executed but still has a few modules to go through
             * from an asp.net perspective.  We filter the content here and all of the javascript renders correctly.*/
            context.PostReleaseRequestState += new EventHandler(context_PostReleaseRequestState);
        }

        void context_PostReleaseRequestState(object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication)sender;

            if (app.Request["HTTP_X_MICROSOFTAJAX"] != null)
                return;

            // fix to handle caching appropriately
            // see http://www.pocketsoap.com/weblog/2003/07/1330.html
            // Note, this header is added only when the request
            // has the possibility of being compressed...
            // i.e. it is not added when the request is excluded from
            // compression by CompressionLevel, Path, or MimeType            
            string realPath = "";
            Configuration settings = null;
            // get the config settings
            if (app.Context.Cache["DCCompressModuleConfig"] == null)
            {
                settings = (Configuration)ConfigurationManager.GetSection("DCWeb/HttpCompress");
                app.Context.Cache["DCCompressModuleConfig"] = settings;
            }
            else
                settings = (Configuration)app.Context.Cache["DCCompressModuleConfig"];
            if (settings != null)
            {
                app.Context.Cache.Insert("DCCompressModuleConfig", settings);
                // skip if the CompressionLevel is set to 'None'
                if (settings.CompressionType == CompressionType.None)
                    return;
                realPath = app.Request.Path.Remove(0, app.Request.ApplicationPath.Length);
                realPath = (realPath.StartsWith("/")) ? realPath.Remove(0, 1) : realPath;

                bool isIncludedPath, isIncludedMime;

                isIncludedPath = (settings.IncludedPaths.Contains(realPath) | settings.IncludedPaths.Contains("~/" + realPath));
                isIncludedMime = (settings.IncludedMimeTypes.Contains(app.Response.ContentType));

                // path was not included, so skip it
                if (!isIncludedPath && !isIncludedMime)
                    return;

                // skip if the file path excludes compression
                if (settings.ExcludedPaths.Contains(realPath) | settings.ExcludedPaths.Contains("~/" + realPath))
                    return;

                // skip if the MimeType excludes compression
                if (settings.ExcludedMimeTypes.Contains(app.Response.ContentType))
                    return;
            }
            app.Context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;
            string acceptedTypes = app.Request.Headers["Accept-Encoding"];

            // if we couldn't find the header, bail out
            if (acceptedTypes == null)
                return;

            // Current response stream

            //Stream baseStream = app.Response.Filter;
            CompressionPageFilter filter = new CompressionPageFilter(app.Response.Filter);
            filter.App = app;
            app.Response.Filter = filter;

            // check for buggy versions of Internet Explorer
            if (app.Context.Request.Browser.Browser == "IE")
            {
                if (app.Context.Request.Browser.MajorVersion < 6)
                    return;
                else if (app.Context.Request.Browser.MajorVersion == 6 &&
                    !string.IsNullOrEmpty(app.Context.Request.ServerVariables["HTTP_USER_AGENT"]) &&
                    app.Context.Request.ServerVariables["HTTP_USER_AGENT"].Contains("EV1"))
                    return;
            }

            // If there are more than one possibility offered by the browser default to the preffered one from the web.config
            // If nothing is specified in the web.config default to GZip
            acceptedTypes = acceptedTypes.ToLower();
            if ((acceptedTypes.Contains("gzip") || acceptedTypes.Contains("x-gzip") || acceptedTypes.Contains("*")) && (settings.CompressionType != CompressionType.Deflate))
                filter.Compress = "gzip";
            else if (acceptedTypes.Contains("deflate"))
                filter.Compress = "deflate";
            if (filter.Compress != "none")
                app.Response.AppendHeader("Content-Encoding", filter.Compress);
        }





        #endregion


        #region Stream filter

        private class CompressionPageFilter : Stream
        {
            private HttpApplication app;
            public HttpApplication App
            {
                get { return app; }
                set { app = value; }
            }
            private string compress = "none";
            public string Compress
            {
                get { return compress; }
                set { compress = value; }
            }
            StringBuilder responseHtml;

            const string _cssPattern = "(?<HTML><link[^>]*href\\s*=\\s*[\\\"\\']?(?<HRef>[^\"'>\\s]*)[\\\"\\']?[^>]*>)";
            const string _jsPattern = "(?<HTML><script[^>]*src\\s*=\\s*[\\\"\\']?(?<SRC>[^\"'>\\s]*)[\\\"\\']?[^>]*></script>)";
            public CompressionPageFilter(Stream sink)
            {
                _sink = sink;
                responseHtml = new StringBuilder();
            }

            private Stream _sink;

            #region Properites

            public override bool CanRead
            {
                get { return true; }
            }

            public override bool CanSeek
            {
                get { return true; }
            }

            public override bool CanWrite
            {
                get { return true; }
            }

            public override void Flush()
            {
                _sink.Flush();
            }

            public override long Length
            {
                get { return 0; }
            }

            private long _position;
            public override long Position
            {
                get { return _position; }
                set { _position = value; }
            }

            #endregion

            #region Methods

            public override int Read(byte[] buffer, int offset, int count)
            {
                return _sink.Read(buffer, offset, count);
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                return _sink.Seek(offset, origin);
            }

            public override void SetLength(long value)
            {
                _sink.SetLength(value);
            }

            public override void Close()
            {
                _sink.Close();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                string strBuffer = UTF8Encoding.UTF8.GetString(buffer, offset, count);

                // ---------------------------------
                // Wait for the closing </html> tag
                // ---------------------------------
                Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);

                responseHtml.Append(strBuffer);

                if (eof.IsMatch(strBuffer))
                {
                    // when compressing the html, some end characters are cut off.  Add some spaces so it cuts the spaces off instead of important characters
                    responseHtml.Append(Environment.NewLine + Environment.NewLine + Environment.NewLine + Environment.NewLine + Environment.NewLine + Environment.NewLine + Environment.NewLine + Environment.NewLine);
                    string html = responseHtml.ToString();

                    // replace the css and js with HttpHandlers that compress the output                    
                    html = ReplaceJS(html);
                    html = ReplaceCss(html);

                    byte[] data = UTF8Encoding.UTF8.GetBytes(html);

                    // if compression is enabled
                    if (compress == "gzip")
                    {
                        GZipStream gzip = new GZipStream(_sink, CompressionMode.Compress);
                        gzip.Write(data, 0, data.Length);
                    }
                    else if (compress == "deflate")
                    {
                        DeflateStream deflate = new DeflateStream(_sink, CompressionMode.Compress);
                        deflate.Write(data, 0, data.Length);
                    }
                    else
                        _sink.Write(data, 0, data.Length);
                }
            }

            /// <summary>
            /// Replcase stylesheet links with ones pointing to HttpHandlers that compress and cache the css
            /// </summary>
            /// <param name="html"></param>
            /// <returns></returns>
            public string ReplaceCss(string html)
            {
                // create a list of the stylesheets
                List<string> stylesheets = new List<string>();
                // create a dictionary used for combining css in the same directory
                Dictionary<string, List<string>> css = new Dictionary<string, List<string>>();

                // create a base uri which will be used to get the uris to the css
                Uri baseUri = new Uri(app.Request.Url.AbsoluteUri);

                // loop through each match
                foreach (Match match in Regex.Matches(html, _cssPattern, RegexOptions.IgnoreCase))
                {
                    // this is the enire match and will be used to replace the link
                    string linkHtml = match.Groups[0].Value;
                    // this is the href of the link
                    string href = match.Groups[2].Value;

                    // get a uri from the base uri, this will resolve any relative and absolute links
                    Uri uri = new Uri(baseUri, href);
                    string file = "";
                    // check to see if it is a link to a local file
                    if (uri.Host == baseUri.Host)
                    {
                        // check to see if it is local to the application
                        if (uri.AbsolutePath.ToLower().StartsWith(app.Context.Request.ApplicationPath.ToLower()))
                        {
                            // this combines css files in the same directory into one file (actual combining done in HttpHandler)
                            int index = uri.AbsolutePath.LastIndexOf("/");
                            string path = uri.AbsolutePath.Substring(0, index + 1);
                            file = uri.AbsolutePath.Substring(index + 1);
                            if (!css.ContainsKey(path))
                                css.Add(path, new List<string>());
                            css[path].Add(file + (href.Contains("?") ? href.Substring(href.IndexOf("?")) : ""));
                            // replace the origianl links with blanks
                            html = html.Replace(linkHtml, "");
                            // continue to next link
                            continue;
                        }
                        else
                            file = uri.AbsolutePath + uri.Query;
                    }
                    else
                        file = uri.AbsoluteUri;
                    string newLinkHtml = linkHtml.Replace(href, "css.axd?files=" + file);

                    // just replace the link with the new link
                    html = html.Replace(linkHtml, newLinkHtml);
                }

                StringBuilder link = new StringBuilder();
                link.AppendLine("");
                foreach (string key in css.Keys)
                {
                    link.AppendLine(string.Format("<link href='{0}css.axd?files={1}' type='text/css' rel='stylesheet' />", key, string.Join(",", css[key].ToArray())));

                }

                // find the head tag and insert css in the head tag
                int x = html.IndexOf("<head");
                int num = 0;
                if (x > -1)
                {
                    num = html.Substring(x).IndexOf(">");
                    html = html.Insert(x + num + 1, link.ToString());
                }
                return html;
            }

            /// <summary>
            /// Replcase javascript links with ones pointing to HttpHandlers that compress and cache the javascript
            /// </summary>
            /// <param name="html"></param>
            /// <returns></returns>
            public string ReplaceJS(string html)
            {
                // if the javascript is in the head section of the html, then try to combine the javascript into one
                int start, end;
                if (html.Contains("<head") && html.Contains("</head>"))
                {
                    start = html.IndexOf("<head");
                    end = html.IndexOf("</head>");
                    string head = html.Substring(start, end - start);

                    head = ReplaceJSInHead(head);

                    html = html.Substring(0, start) + head + html.Substring(end);
                }

                // javascript that is referenced in the body is usually used to write content to the page via javascript, 
                // we don't want to combine these and place them in the header since it would cause problems
                // or it is a WebResource.axd or ScriptResource.axd
                if (html.Contains("<body") && html.Contains("</body>"))
                {
                    start = html.IndexOf("<body");
                    end = html.IndexOf("</body>");
                    string head = html.Substring(start, end - start);

                    head = ReplaceJSInBody(head);

                    html = html.Substring(0, start) + head + html.Substring(end);
                }

                return html;
            }

            /// <summary>
            /// Replaces the js in the head tag. (see ReplaceCss for comments)
            /// </summary>
            /// <param name="html"></param>
            /// <returns></returns>
            public string ReplaceJSInHead(string html)
            {
                List<string> javascript = new List<string>();
                Dictionary<string, List<string>> js = new Dictionary<string, List<string>>();

                Uri baseUri = new Uri(app.Request.Url.AbsoluteUri);
                foreach (Match match in Regex.Matches(html, _jsPattern, RegexOptions.IgnoreCase))
                {
                    string linkHtml = match.Groups[0].Value;
                    string src = match.Groups[2].Value;

                    Uri uri = new Uri(baseUri, src);
                    if (!Path.GetExtension(uri.AbsolutePath).Equals("js") && uri.AbsolutePath.Contains("WebResource.axd"))
                        continue;
                    if (uri.Host == baseUri.Host)
                    {
                        if (uri.AbsolutePath.ToLower().StartsWith(app.Context.Request.ApplicationPath.ToLower()))
                        {
                            int index = uri.AbsolutePath.LastIndexOf("/");
                            string path = uri.AbsolutePath.Substring(0, index + 1);
                            string file = uri.AbsolutePath.Substring(index + 1);
                            if (!js.ContainsKey(path))
                                js.Add(path, new List<string>());
                            js[path].Add(file + (src.Contains("?") ? src.Substring(src.IndexOf("?")) : ""));
                        }
                        else
                            javascript.Add(uri.AbsolutePath + uri.Query);

                    }
                    else
                        javascript.Add(uri.AbsoluteUri);
                    html = html.Replace(linkHtml, "");
                }

                int x = html.IndexOf("<head");
                int num = html.Substring(x).IndexOf(">");
                string link = "";

                foreach (string key in js.Keys)
                {
                    link = string.Format("<script src='{0}js.axd?files={1}' type='text/javascript' ></script>", key, string.Join(",", js[key].ToArray()));
                    html = html.Insert(x + num + 1, link + Environment.NewLine);

                }
                if (javascript.Count > 0)
                {
                    link = string.Format("<script src='js.axd?files={0}' type='text/javascript' /></script>", string.Join(",", javascript.ToArray()));
                    html = html.Insert(x + num + 1, link + Environment.NewLine);
                }
                return html;
            }

            /// <summary>
            /// Replaces the js in the body. (see ReplaceCss for comments)
            /// </summary>
            /// <param name="html"></param>
            /// <returns></returns>
            public string ReplaceJSInBody(string html)
            {
                Uri baseUri = new Uri(app.Request.Url.AbsoluteUri);
                foreach (Match match in Regex.Matches(html, _jsPattern, RegexOptions.IgnoreCase))
                {
                    string linkHtml = match.Groups[0].Value;
                    string src = match.Groups[2].Value;


                    Uri uri = new Uri(baseUri, src);
                    if (!uri.AbsolutePath.EndsWith(".js") && !uri.AbsolutePath.Contains("WebResource.axd"))
                        continue;
                    string file = "";
                    string path = "";
                    if (uri.Host == baseUri.Host)
                    {
                        if (uri.AbsolutePath.ToLower().StartsWith(app.Context.Request.ApplicationPath.ToLower()))
                        {
                            int index = uri.AbsolutePath.LastIndexOf("/");
                            path = uri.AbsolutePath.Substring(0, index + 1);
                            file = uri.AbsolutePath.Substring(index + 1) + (src.Contains("?") ? src.Substring(src.IndexOf("?")) : "");
                        }
                        else
                            file = uri.AbsolutePath + uri.Query;
                    }
                    else
                        file = uri.AbsoluteUri;
                    string newLinkHtml = linkHtml.Replace(src, path + "js.axd?files=" + file);
                    html = html.Replace(linkHtml, newLinkHtml);
                }
                return html;
            }

            #endregion

        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions