扩展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 = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
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。有兴趣的可以去看看。