扩展Bundle支持动态Bundle和javascript混淆
两个目的:
- 支持动态页面上的Bundle,而不必每次在Global中添加Bundle。
- 支持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
@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。有兴趣的可以去看看。