// ==UserScript==
// @name jQuery Hook
// @namespace https://github.com/JSREI/jQuery-hook
// @version 0.4
// @description 用于快速定位使用jQuery绑定到DOM元素上的事件的代码的真实位置,辅助逆向分析。
// @document https://github.com/JSREI/jQuery-hook
// @author CC11001100
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
// 可自定义的一个变量前缀,尽量唯一有区分度即可,可以替换为为自己的ID
const globalUniqPrefix = "cc11001100";
// 用于控制打印在控制台的消息的大小
const consoleLogFontSize = 12;
// ----------------------------------------------- -----------------------------------------------------------------
/**
* 用于统一构建待颜色的日志输出,采用构建者模式
*
* from: https://github.com/JSREI/js-color-log
*/
class ColorLogBuilder {
/**
* 创建一条日志,调用show()方法将其打印到控制台
*
* 因为认为字体颜色是没有区分度的,所以这里就不支持指定字体的颜色,字体恒定为黑色
*
* @param normalTextBackgroundColor {string} 此条日志中普通文本的背景色
* @param highlightTextBackgroundColor {string} 此条日志中要高亮的文本的背景色
* @param _consoleLogFontSize {string} 日志的大小
*/
constructor(normalTextBackgroundColor = "#FFFFFF", highlightTextBackgroundColor = "#FFFFFF", _consoleLogFontSize = consoleLogFontSize) {
this.normalTextBackgroundColor = normalTextBackgroundColor;
this.highlightTextBackgroundColor = highlightTextBackgroundColor;
this.consoleLogFontSize = _consoleLogFontSize;
this.messageArray = [];
// 每天日志都使用统一的前缀,在创建的时候就设置好
// 先是一个日期,然后是插件的名字,以便与其它工具的输出相区分
// 此处的统一前缀自行修改,因为使用的时候都是拷贝过去的
this.append(`[${this.nowTimeString()}] `).append("jQuery Hook: ");
}
/**
* 往日志中追加普通类型的信息
*
* @param msg {string}
* @return {ColorLogBuilder}
*/
append(msg) {
this.appendNormal(msg);
return this;
}
/**
* 往日志中追加普通类型的信息
*
* @param msg {string}
* @return {ColorLogBuilder}
*/
appendNormal(msg) {
this.messageArray.push(`color: black; background: ${this.normalTextBackgroundColor}; font-size: ${this.consoleLogFontSize}px;`);
this.messageArray.push(msg);
return this;
}
/**
* 往日志中追加高亮的内容
*
* @param msg {string}
*/
appendHighlight(msg) {
this.messageArray.push(`color: black; background: ${this.highlightTextBackgroundColor}; font-size: ${this.consoleLogFontSize}px; font-weight: bold;`);
this.messageArray.push(msg);
return this;
}
/**
* 把当前这条日志打印出来
*/
show() {
console.log(this.genFormatArray(this.messageArray), ...this.messageArray);
}
nowTimeString(fmt = "yyyy-MM-dd HH:mm:ss") {
const now = new Date();
let o = {
"M+": now.getMonth() + 1, "d+": now.getDate(), //日
"H+": now.getHours(), //小时
"m+": now.getMinutes(), //分
"s+": now.getSeconds(), //秒
"q+": Math.floor((now.getMonth() + 3) / 3), //季度
"S": now.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (now.getFullYear() + "").substr(4 - RegExp.$1.length));
for (let k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
genFormatArray(messageAndStyleArray) {
const formatArray = [];
for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) {
formatArray.push("%c%s");
}
return formatArray.join("");
}
}
// ----------------------------------------------- -----------------------------------------------------------------
// 在第一次初始化jQuery的时候添加Hook,jQuery初始化的时候会添加一个名为$的全局变量,在添加这个变量的时候对其动一些手脚
Object.defineProperty(window, "$", {
set: $ => {
// 为jquery的各种方法添加Hook
try {
addHook($);
} catch (e) {
new ColorLogBuilder("#FF6766", "#E50000")
.append("add hook error, msg = ")
.appendHighlight(e)
.show();
}
// 删除set描述符拦截,恢复正常赋值,假装啥都没发生过,但实际上已经狸猫换太子了...
delete window["$"];
window["$"] = $;
}, configurable: true
});
/**
* 为jquery添加一些hook,等会儿使用jquery为dom元素绑定事件的话就会被捕获到
* @param $
*/
function addHook($) {
addEventHook($);
addAjaxHook($);
new ColorLogBuilder("#65CC66", "#669934")
.append("在当前页面上检测到jQuery的加载,添加jQuery Hook完成")
.show();
}
/**
* 增加Ajax Hook
*
* @param $
*/
function addAjaxHook($) {
if (!$["ajaxSetup"]) {
new ColorLogBuilder("#FF6766", "#E50000")
.appendHighlight("$不是jQuery对象,没有 ajaxSetup 属性,因此不添加Ajax Hook")
.show();
return;
}
const oldAjaxSetUp = $.ajaxSetup;
$.ajaxSetup = function () {
try {
if (arguments.length === 1) {
const {formatEventName, eventFuncGlobalName} = storeToWindow("ajaxSetup", arguments[0]);
new ColorLogBuilder("#65CC66", "#669934")
.append("检测到ajaxSetup全局拦截器设置请求参数,已经挂载到全局变量:")
.appendHighlight(eventFuncGlobalName)
.show();
}
} catch (e) {
console.error(e);
}
return oldAjaxSetUp.apply(this, arguments);
}
}
/**
* 增加事件Hook
*
* @param $
*/
function addEventHook($) {
if (!$["fn"]) {
new ColorLogBuilder("#FF6766", "#E50000")
.appendHighlight("$不是jQuery对象,没有 fn 属性,因此不添加 Event Hook")
.show();
return;
}
// 一些比较通用的事件的拦截
const eventNameList = ["click", "dblclick", "blur", "change", "contextmenu", "error", "focus", "focusin", "focusout", "hover", "holdReady", "proxy", "ready", "keydown", "keypress", "keyup", "live", "load", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup"];
for (let eventName of eventNameList) {
const old = $.fn[eventName];
$.fn[eventName] = function () {
try {
setEventFunctionNameToDomObjectAttribute(this, eventName, arguments[0]);
} catch (e) {
new ColorLogBuilder("#FF6766", "#E50000")
.appendHighlight(`为jQuery添加${eventName}类型的事件的Hook时发生错误: ${e}`)
.show();
}
return old.apply(this, arguments);
}
}
// on,不仅是内置事件类型,还有可能有一些自定义的事件类型
// https://api.jquery.com/on/
const fnOnHolder = $.fn.on;
$.fn.on = function () {
try {
const eventName = arguments[0];
let eventFunction = undefined;
for (let x of arguments) {
if (x instanceof Function) {
eventFunction = x;
break;
}
}
if (eventFunction instanceof Function) {
setEventFunctionNameToDomObjectAttribute(this, eventName, eventFunction);
}
} catch (e) {
new ColorLogBuilder("#FF6766", "#E50000")
.appendHighlight(`为jQuery添加on方法的Hook时发生错误: ${e}`)
.show();
}
return fnOnHolder.apply(this, arguments);
}
// TODO 还有delegate之类的比较隐晦的绑定事件的方式
}
/**
* 为绑定了jquery事件的dom元素添加元素,提示所绑定的事件与对应的函数代码的全局变量的名称,只需要复制粘贴跟进去即可
* 注意,有可能会为同一个元素重复绑定相同的事件
*
* @param domObject
* @param eventName
* @param eventFunction
*/
function setEventFunctionNameToDomObjectAttribute(domObject, eventName, eventFunction) {
eventName.split(' ').map((eventName) => {
const {formatEventName, eventFuncGlobalName} = storeToWindow(eventName, eventFunction);
const attrName = `${globalUniqPrefix}-jQuery-${formatEventName}-event-function`;
if (domObject.attr(attrName)) {
domObject.attr(attrName + "-" + new Date().getTime(), eventFuncGlobalName);
} else {
domObject.attr(attrName, eventFuncGlobalName);
}
})
}
// ----------------------------------------------- -----------------------------------------------------------------
// 用于缓存事件函数到全局变量的映射关系
// <事件函数, 全局变量>
const eventFuncCacheMap = new Map();
/**
* 为事件的函数绑定一个全局变量,如果之前已经绑定过了则返回之前的
*
* @param eventName {string}
* @param eventFunc {Function}
* @return {{string, string}} 事件名和其对应的函数绑定到的全局变量
*/
function storeToWindow(eventName, eventFunc) {
if (eventFunc in eventFuncCacheMap) {
return eventFuncCacheMap[eventFunc];
}
// 注意,事件名可能会包含一些非法的字符,所以需要转义
// cc11001100-jquery-$destroy-event-function
const formatEventName = safeSymbol(eventName);
const eventFuncGlobalName = globalUnique(formatEventName);
window[eventFuncGlobalName] = eventFunc;
eventFuncCacheMap[eventFunc] = eventFuncGlobalName;
return {
formatEventName, eventFuncGlobalName,
};
}
/***
* 将事件名称转为合法的变量名称
*
* @param name
*/
function safeSymbol(name) {
const replaceMap = {
".": "_dot_",
"$": "_dollar_",
"-": "_dash_",
" ": "_whitespace_"
};
let newName = "";
for (let c of name) {
if (c in replaceMap) {
newName += replaceMap[c];
} else if (isOkVarChar(c)) {
newName += c;
}
}
return newName;
}
/**
* 判断字符是否是合法的变量名字符
*
* @param c {string}
* @returns {boolean}
*/
function isOkVarChar(c) {
return (/^[a-zA-Z0-9]$/).test(c);
}
// ----------------------------------------------- -----------------------------------------------------------------
// 每个事件一个独立的自增id
const addressIdGeneratorMap = {};
/**
* 为给定的事件生成一个全局唯一的标识,这个标识中会带上事件类型以方便区分不同事件
*
* @param eventName {string}
*/
function globalUnique(eventName) {
const id = (addressIdGeneratorMap[eventName] || 0) + 1;
addressIdGeneratorMap[eventName] = id;
return `${globalUniqPrefix}__${eventName}__${id}`;
}
// ----------------------------------------------- -----------------------------------------------------------------
/**
* 解析当前代码的位置,以便能够直接定位到事件触发的代码位置
*
* @param keyword {string}
* @returns {string}
*/
function getCodeLocation(keyword = "cc11001100") {
const callstack = new Error().stack.split("\n");
while (callstack.length && callstack[0].indexOf(keyword) === -1) {
callstack.shift();
}
callstack.shift();
// callstack.shift();
return callstack[0].trim();
}
})();