扩展Bundle支持动态Bundle和javascript混淆

扩展Bundle支持动态Bundle和javascript混淆


两个目的:

  1. 支持动态页面上的Bundle,而不必每次在Global中添加Bundle。
  2. 支持Javascript混淆

ASP.NET MVC自带Bundle


通常ASP.NET MVC 4.0以后自带的Bundle如下:
BundleConfig.RegisterBundles(BundleTable.Bundles);
public class BundleConfig
    {
        // For more information on Bundling, visit http://go.microsoft.com/fwlink/?LinkId=254725
        public static void RegisterBundles(BundleCollection bundles)
        {
            // Use the development version of Modernizr to develop with and learn from. Then, when you‘re
            // ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));

            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

#if !DEBUG
            BundleTable.EnableOptimizations = true;
#endif
        }
    }

页面上的使用:
@Scripts.Render("~/bundles/jquery")

新动态Bundle


对于有些不是很重要的页面,觉得不必为此页面特意创建一个Bundle,此时在页面上动态创建Bundle会更方便一些。如,在页面上使用:
@Html.Style("~/Scripts/JQueryUI2/themes/smoothness/jquery.ui.theme.css", "~/Scripts/JQueryUI2/themes/smoothness/jquery.ui.menu.css")
        @Html.Script("~/Scripts/JQueryUI2/ui/jquery.ui.core.js", "~/Scripts/JQueryUI2/ui/jquery.ui.position.js", "~/Scripts/JQueryUI2/ui/jquery.ui.widget.js", "~/Scripts/JQueryUI2/ui/jquery.ui.menu.js")

当然上面Html.Style是自己创建的扩展,下面贴出扩展的源码:
public static class Extension
    {
        public static IHtmlString Script(this HtmlHelper helper, params string[] urls)
        {
            var bundleDirectory = "~/bundles/" + MakeBundleName("js", urls);
            var bundle = BundleTable.Bundles.GetBundleFor(bundleDirectory);
            if (bundle == null)
            {
                var transform = new JavascriptObfuscator();
                bundle = new ScriptBundle(bundleDirectory).Include(urls);
                bundle.Transforms.Add(transform);
                BundleTable.Bundles.Add(bundle);
            }
            return Scripts.Render(bundleDirectory);
        }

        public static IHtmlString Style(this HtmlHelper helper, params string[] urls)
        {
            var bundleDirectory = "~/bundles/" + MakeBundleName("css", urls);
            var bundle=BundleTable.Bundles.GetBundleFor(bundleDirectory);
            if (bundle == null)
            {
                bundle = new StyleBundle(bundleDirectory).Include(urls);
                BundleTable.Bundles.Add(bundle);
            }
            return Styles.Render(bundleDirectory);
        }

        private static string MakeBundleName(string type, params string[] urls)
        {
            var array =
                urls.SelectMany(url => url.Split(‘/‘))
                    .SelectMany(url => url.Split(‘.‘))
                    .Distinct()
                    .Except(new[] {"~", type});

            return string.Join("-", array);
        }
    }

相信上面的代码简简易懂,不需要多作解释。上面只是一个思路,人们可以随意发挥你的创作。只是有一点要说的是,我添加了一个Javascript混淆器,为了生成像如下一样的代码:
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromChar

混淆器代码是网上找的,如下:
/// <summary>
    /// Packs a javascript file into a smaller area, removing unnecessary characters from the output.
    /// </summary>
    public class ECMAScriptPacker : IHttpHandler
    {
        /// <summary>
        /// The encoding level to use. See http://dean.edwards.name/packer/usage/ for more info.
        /// </summary>
        public enum PackerEncoding { None = 0, Numeric = 10, Mid = 36, Normal = 62, HighAscii = 95 };

        private PackerEncoding encoding = PackerEncoding.Normal;
        private bool fastDecode = true;
        private bool specialChars = false;
        private bool enabled = true;

        string IGNORE = "$1";

        /// <summary>
        /// The encoding level for this instance
        /// </summary>
        public PackerEncoding Encoding
        {
            get { return encoding; }
            set { encoding = value; }
        }

        /// <summary>
        /// Adds a subroutine to the output to speed up decoding
        /// </summary>
        public bool FastDecode
        {
            get { return fastDecode; }
            set { fastDecode = value; }
        }

        /// <summary>
        /// Replaces special characters
        /// </summary>
        public bool SpecialChars
        {
            get { return specialChars; }
            set { specialChars = value; }
        }

        /// <summary>
        /// Packer enabled
        /// </summary>
        public bool Enabled
        {
            get { return enabled; }
            set { enabled = value; }
        }

        public ECMAScriptPacker()
        {
            Encoding = PackerEncoding.Normal;
            FastDecode = true;
            SpecialChars = false;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="encoding">The encoding level for this instance</param>
        /// <param name="fastDecode">Adds a subroutine to the output to speed up decoding</param>
        /// <param name="specialChars">Replaces special characters</param>
        public ECMAScriptPacker(PackerEncoding encoding, bool fastDecode, bool specialChars)
        {
            Encoding = encoding;
            FastDecode = fastDecode;
            SpecialChars = specialChars;
        }

        /// <summary>
        /// Packs the script
        /// </summary>
        /// <param name="script">the script to pack</param>
        /// <returns>the packed script</returns>
        public string Pack(string script)
        {
            if (enabled)
            {
                script += "\n";
                script = basicCompression(script);
                if (SpecialChars)
                    script = encodeSpecialChars(script);
                if (Encoding != PackerEncoding.None)
                    script = encodeKeywords(script);
            }
            return script;
        }

        //zero encoding - just removal of whitespace and comments
        private string basicCompression(string script)
        {
            ParseMaster parser = new ParseMaster();
            // make safe
            parser.EscapeChar = ‘\\‘;
            // protect strings
            parser.Add("‘[^‘\\n\\r]*‘", IGNORE);
            parser.Add("\"[^\"\\n\\r]*\"", IGNORE);
            // remove comments
            parser.Add("\\/\\/[^\\n\\r]*[\\n\\r]");
            parser.Add("\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\/");
            // protect regular expressions
            parser.Add("\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)", "$2");
            parser.Add("[^\\w\\$\\/‘\"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?", IGNORE);
            // remove: ;;; doSomething();
            if (specialChars)
                parser.Add(";;[^\\n\\r]+[\\n\\r]");
            // remove redundant semi-colons
            parser.Add(";+\\s*([};])", "$2");
            // remove white-space
            parser.Add("(\\b|\\$)\\s+(\\b|\\$)", "$2 $3");
            parser.Add("([+\\-])\\s+([+\\-])", "$2 $3");
            parser.Add("\\s+");
            // done
            return parser.Exec(script);
        }

        WordList encodingLookup;
        private string encodeSpecialChars(string script)
        {
            ParseMaster parser = new ParseMaster();
            // replace: $name -> n, $$name -> na
            parser.Add("((\\$+)([a-zA-Z\\$_]+))(\\d*)",
                new ParseMaster.MatchGroupEvaluator(encodeLocalVars));

            // replace: _name -> _0, double-underscore (__name) is ignored
            Regex regex = new Regex("\\b_[A-Za-z\\d]\\w*");

            // build the word list
            encodingLookup = analyze(script, regex, new EncodeMethod(encodePrivate));

            parser.Add("\\b_[A-Za-z\\d]\\w*", new ParseMaster.MatchGroupEvaluator(encodeWithLookup));

            script = parser.Exec(script);
            return script;
        }

        private string encodeKeywords(string script)
        {
            // escape high-ascii values already in the script (i.e. in strings)
            if (Encoding == PackerEncoding.HighAscii) script = escape95(script);
            // create the parser
            ParseMaster parser = new ParseMaster();
            EncodeMethod encode = getEncoder(Encoding);

            // for high-ascii, don‘t encode single character low-ascii
            Regex regex = new Regex(
                    (Encoding == PackerEncoding.HighAscii) ? "\\w\\w+" : "\\w+"
                );
            // build the word list
            encodingLookup = analyze(script, regex, encode);

            // encode
            parser.Add((Encoding == PackerEncoding.HighAscii) ? "\\w\\w+" : "\\w+",
                new ParseMaster.MatchGroupEvaluator(encodeWithLookup));

            // if encoded, wrap the script in a decoding function
            return (script == string.Empty) ? "" : bootStrap(parser.Exec(script), encodingLookup);
        }

        private string bootStrap(string packed, WordList keywords)
        {
            // packed: the packed script
            packed = "‘" + escape(packed) + "‘";

            // ascii: base for encoding
            int ascii = Math.Min(keywords.Sorted.Count, (int)Encoding);
            if (ascii == 0)
                ascii = 1;

            // count: number of words contained in the script
            int count = keywords.Sorted.Count;

            // keywords: list of words contained in the script
            foreach (object key in keywords.Protected.Keys)
            {
                keywords.Sorted[(int)key] = "";
            }
            // convert from a string to an array
            StringBuilder sbKeywords = new StringBuilder("‘");
            foreach (string word in keywords.Sorted)
                sbKeywords.Append(word + "|");
            sbKeywords.Remove(sbKeywords.Length - 1, 1);
            string keywordsout = sbKeywords.ToString() + "‘.split(‘|‘)";

            string encode;
            string inline = "c";

            switch (Encoding)
            {
                case PackerEncoding.Mid:
                    encode = "function(c){return c.toString(36)}";
                    inline += ".toString(a)";
                    break;
                case PackerEncoding.Normal:
                    encode = "function(c){return(c<a?\"\":e(parseInt(c/a)))+" +
                        "((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))}";
                    inline += ".toString(a)";
                    break;
                case PackerEncoding.HighAscii:
                    encode = "function(c){return(c<a?\"\":e(c/a))+" +
                        "String.fromCharCode(c%a+161)}";
                    inline += ".toString(a)";
                    break;
                default:
                    encode = "function(c){return c}";
                    break;
            }

            // decode: code snippet to speed up decoding
            string decode = "";
            if (fastDecode)
            {
                decode = "if(!‘‘.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return‘\\\\w+‘};c=1;}";
                if (Encoding == PackerEncoding.HighAscii)
                    decode = decode.Replace("\\\\w", "[\\xa1-\\xff]");
                else if (Encoding == PackerEncoding.Numeric)
                    decode = decode.Replace("e(c)", inline);
                if (count == 0)
                    decode = decode.Replace("c=1", "c=0");
            }

            // boot function
            string unpack = "function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp(‘\\\\b‘+e(c)+‘\\\\b‘,‘g‘),k[c]);return p;}";
            Regex r;
            if (fastDecode)
            {
                //insert the decoder
                r = new Regex("\\{");
                unpack = r.Replace(unpack, "{" + decode + ";", 1);
            }

            if (Encoding == PackerEncoding.HighAscii)
            {
                // get rid of the word-boundries for regexp matches
                r = new Regex("‘\\\\\\\\b‘\\s*\\+|\\+\\s*‘\\\\\\\\b‘");
                unpack = r.Replace(unpack, "");
            }
            if (Encoding == PackerEncoding.HighAscii || ascii > (int)PackerEncoding.Normal || fastDecode)
            {
                // insert the encode function
                r = new Regex("\\{");
                unpack = r.Replace(unpack, "{e=" + encode + ";", 1);
            }
            else
            {
                r = new Regex("e\\(c\\)");
                unpack = r.Replace(unpack, inline);
            }
            // no need to pack the boot function since i‘ve already done it
            string _params = "" + packed + "," + ascii + "," + count + "," + keywordsout;
            if (fastDecode)
            {
                //insert placeholders for the decoder
                _params += ",0,{}";
            }
            // the whole thing
            return "eval(" + unpack + "(" + _params + "))\n";
        }

        private string escape(string input)
        {
            Regex r = new Regex("([\\\\‘])");
            return r.Replace(input, "\\$1");
        }

        private EncodeMethod getEncoder(PackerEncoding encoding)
        {
            switch (encoding)
            {
                case PackerEncoding.Mid:
                    return new EncodeMethod(encode36);
                case PackerEncoding.Normal:
                    return new EncodeMethod(encode62);
                case PackerEncoding.HighAscii:
                    return new EncodeMethod(encode95);
                default:
                    return new EncodeMethod(encode10);
            }
        }

        private string encode10(int code)
        {
            return code.ToString();
        }

        //lookups seemed like the easiest way to do this since 
        // I don‘t know of an equivalent to .toString(36)
        private static string lookup36 = "0123456789abcdefghijklmnopqrstuvwxyz";

        private string encode36(int code)
        {
            string encoded = "";
            int i = 0;
            do
            {
                int digit = (code / (int)Math.Pow(36, i)) % 36;
                encoded = lookup36[digit] + encoded;
                code -= digit * (int)Math.Pow(36, i++);
            } while (code > 0);
            return encoded;
        }

        private static string lookup62 = lookup36 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

        private string encode62(int code)
        {
            string encoded = "";
            int i = 0;
            do
            {
                int digit = (code / (int)Math.Pow(62, i)) % 62;
                encoded = lookup62[digit] + encoded;
                code -= digit * (int)Math.Pow(62, i++);
            } while (code > 0);
            return encoded;
        }

        private static string lookup95 = "?¢£¤¥|§¨©a??-®ˉ°±23′μ?·?1o?????àá??????èéê?ìí??D?òó???×?ùú?üYT?àáa?????èéê?ìí??e?òó???÷?ùú?üyt?";

        private string encode95(int code)
        {
            string encoded = "";
            int i = 0;
            do
            {
                int digit = (code / (int)Math.Pow(95, i)) % 95;
                encoded = lookup95[digit] + encoded;
                code -= digit * (int)Math.Pow(95, i++);
            } while (code > 0);
            return encoded;
        }

        private string escape95(string input)
        {
            Regex r = new Regex("[\xa1-\xff]");
            return r.Replace(input, new MatchEvaluator(escape95Eval));
        }

        private string escape95Eval(Match match)
        {
            return "\\x" + ((int)match.Value[0]).ToString("x"); //return hexadecimal value
        }

        private string encodeLocalVars(Match match, int offset)
        {
            int length = match.Groups[offset + 2].Length;
            int start = length - Math.Max(length - match.Groups[offset + 3].Length, 0);
            return match.Groups[offset + 1].Value.Substring(start, length) +
                match.Groups[offset + 4].Value;
        }

        private string encodeWithLookup(Match match, int offset)
        {
            return (string)encodingLookup.Encoded[match.Groups[offset].Value];
        }

        private delegate string EncodeMethod(int code);

        private string encodePrivate(int code)
        {
            return "_" + code;
        }

        private WordList analyze(string input, Regex regex, EncodeMethod encodeMethod)
        {
            // analyse
            // retreive all words in the script
            MatchCollection all = regex.Matches(input);
            WordList rtrn;
            rtrn.Sorted = new StringCollection(); // list of words sorted by frequency
            rtrn.Protected = new HybridDictionary(); // dictionary of word->encoding
            rtrn.Encoded = new HybridDictionary(); // instances of "protected" words
            if (all.Count > 0)
            {
                StringCollection unsorted = new StringCollection(); // same list, not sorted
                HybridDictionary Protected = new HybridDictionary(); // "protected" words (dictionary of word->"word")
                HybridDictionary values = new HybridDictionary(); // dictionary of charCode->encoding (eg. 256->ff)
                HybridDictionary count = new HybridDictionary(); // word->count
                int i = all.Count, j = 0;
                string word;
                // count the occurrences - used for sorting later
                do
                {
                    word = "$" + all[--i].Value;
                    if (count[word] == null)
                    {
                        count[word] = 0;
                        unsorted.Add(word);
                        // make a dictionary of all of the protected words in this script
                        //  these are words that might be mistaken for encoding
                        Protected["$" + (values[j] = encodeMethod(j))] = j++;
                    }
                    // increment the word counter
                    count[word] = (int)count[word] + 1;
                } while (i > 0);
                /* prepare to sort the word list, first we must protect
                    words that are also used as codes. we assign them a code
                    equivalent to the word itself.
                   e.g. if "do" falls within our encoding range
                        then we store keywords["do"] = "do";
                   this avoids problems when decoding */
                i = unsorted.Count;
                string[] sortedarr = new string[unsorted.Count];
                do
                {
                    word = unsorted[--i];
                    if (Protected[word] != null)
                    {
                        sortedarr[(int)Protected[word]] = word.Substring(1);
                        rtrn.Protected[(int)Protected[word]] = true;
                        count[word] = 0;
                    }
                } while (i > 0);
                string[] unsortedarr = new string[unsorted.Count];
                unsorted.CopyTo(unsortedarr, 0);
                // sort the words by frequency
                Array.Sort(unsortedarr, (IComparer)new CountComparer(count));
                j = 0;
                /*because there are "protected" words in the list
                  we must add the sorted words around them */
                do
                {
                    if (sortedarr[i] == null)
                        sortedarr[i] = unsortedarr[j++].Substring(1);
                    rtrn.Encoded[sortedarr[i]] = values[i];
                } while (++i < unsortedarr.Length);
                rtrn.Sorted.AddRange(sortedarr);
            }
            return rtrn;
        }

        private struct WordList
        {
            public StringCollection Sorted;
            public HybridDictionary Encoded;
            public HybridDictionary Protected;
        }

        private class CountComparer : IComparer
        {
            HybridDictionary count;

            public CountComparer(HybridDictionary count)
            {
                this.count = count;
            }

            #region IComparer Members

            public int Compare(object x, object y)
            {
                return (int)count[y] - (int)count[x];
            }

            #endregion
        }
        #region IHttpHandler Members

        public void ProcessRequest(HttpContext context)
        {
            // try and read settings from config file
            if (System.Configuration.ConfigurationSettings.GetConfig("ecmascriptpacker") != null)
            {
                NameValueCollection cfg =
                    (NameValueCollection)
                    System.Configuration.ConfigurationSettings.GetConfig("ecmascriptpacker");
                if (cfg["Encoding"] != null)
                {
                    switch (cfg["Encoding"].ToLower())
                    {
                        case "none":
                            Encoding = PackerEncoding.None;
                            break;
                        case "numeric":
                            Encoding = PackerEncoding.Numeric;
                            break;
                        case "mid":
                            Encoding = PackerEncoding.Mid;
                            break;
                        case "normal":
                            Encoding = PackerEncoding.Normal;
                            break;
                        case "highascii":
                        case "high":
                            Encoding = PackerEncoding.HighAscii;
                            break;
                    }
                }
                if (cfg["FastDecode"] != null)
                {
                    if (cfg["FastDecode"].ToLower() == "true")
                        FastDecode = true;
                    else
                        FastDecode = false;
                }
                if (cfg["SpecialChars"] != null)
                {
                    if (cfg["SpecialChars"].ToLower() == "true")
                        SpecialChars = true;
                    else
                        SpecialChars = false;
                }
                if (cfg["Enabled"] != null)
                {
                    if (cfg["Enabled"].ToLower() == "true")
                        Enabled = true;
                    else
                        Enabled = false;
                }
            }
            // try and read settings from URL
            if (context.Request.QueryString["Encoding"] != null)
            {
                switch (context.Request.QueryString["Encoding"].ToLower())
                {
                    case "none":
                        Encoding = PackerEncoding.None;
                        break;
                    case "numeric":
                        Encoding = PackerEncoding.Numeric;
                        break;
                    case "mid":
                        Encoding = PackerEncoding.Mid;
                        break;
                    case "normal":
                        Encoding = PackerEncoding.Normal;
                        break;
                    case "highascii":
                    case "high":
                        Encoding = PackerEncoding.HighAscii;
                        break;
                }
            }
            if (context.Request.QueryString["FastDecode"] != null)
            {
                if (context.Request.QueryString["FastDecode"].ToLower() == "true")
                    FastDecode = true;
                else
                    FastDecode = false;
            }
            if (context.Request.QueryString["SpecialChars"] != null)
            {
                if (context.Request.QueryString["SpecialChars"].ToLower() == "true")
                    SpecialChars = true;
                else
                    SpecialChars = false;
            }
            if (context.Request.QueryString["Enabled"] != null)
            {
                if (context.Request.QueryString["Enabled"].ToLower() == "true")
                    Enabled = true;
                else
                    Enabled = false;
            }
            //handle the request
            TextReader r = new StreamReader(context.Request.PhysicalPath);
            string jscontent = r.ReadToEnd();
            r.Close();
            context.Response.ContentType = "text/javascript";
            context.Response.Output.Write(Pack(jscontent));
        }

        public bool IsReusable
        {
            get
            {
                if (System.Configuration.ConfigurationSettings.GetConfig("ecmascriptpacker") != null)
                {
                    NameValueCollection cfg =
                        (NameValueCollection)
                        System.Configuration.ConfigurationSettings.GetConfig("ecmascriptpacker");
                    if (cfg["IsReusable"] != null)
                        if (cfg["IsReusable"].ToLower() == "true")
                            return true;
                }
                return false;
            }
        }

        #endregion
    }

上面代码中有一些作者的信息。


为了使用它,创建一个BundleTransform:
public class JavascriptObfuscator : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {
            var p = new ECMAScriptPacker(ECMAScriptPacker.PackerEncoding.Normal, true, false);

            response.Content = p.Pack(response.Content);
        }
    }

当然这个混淆器可以做为一个Handler使用,不多做说明。

总结


上面代码全部连接起来,就是我想要用的两个功能。这只是为自己的一点点私欲创建的一个小工具,希望对有些人会有帮助。另外提一下,网上有现成的强大的Bundle插件,如Bundle Transformer: YUI 1.8.0。有兴趣的可以去看看。


扩展Bundle支持动态Bundle和javascript混淆

上一篇:Jquery动态添加输入框


下一篇:PHPCMS 错误日志 Only variables should be passed by ...