Storage Monitor/Debugger Hook

用于监控js对localStorage/sessionStorage的任何操作,或者在符合给定条件时进入断点

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Storage Monitor/Debugger Hook
// @namespace    https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/005-storage-hook
// @version      0.1
// @description  用于监控js对localStorage/sessionStorage的任何操作,或者在符合给定条件时进入断点
// @document     https://github.com/CC11001100/crawler-js-hook-framework-public/tree/master/005-storage-hook
// @author       CC11001100
// @match       *://*/*
// @run-at      document-start
// @grant       none
// ==/UserScript==
(() => {

    // 简介: 用于检测、调试浏览器中的localStorage和sessionStorage的任何操作
    // 本工具详细文档见:
    // Storage是什么: https://developer.mozilla.org/zh-CN/docs/Web/API/Storage

    // 修改这里来打断点
    const storageDebuggerList = [
        "947e722bbefb8a455c278113042beadb",

        // 允许使用字符串,字符串用来对name做完全相等的匹配
        // 对LocalStorage或SessionStorage的key为foo-name进行的任何操作都会进入断点
        // "foo-name",

        // 字符串形式的增强版,允许使用正则表达式,正则表达式只用来匹配name
        // /^foo-prefix*/,

        // 这才是一个完整的配置,可以比较精细的打断点
        // {
        //
        //     // storageType { "local" | "session" | "all" }
        //     "storageType": "all",
        //
        //     // operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
        //     "operationType": "all",
        //
        //     // nameFilter { "string" | RegExp | null }
        //     "nameFilter": "foo-name",
        //
        //     // valueFilter { "string" | RegExp | null }
        //     "valueFilter": "foo-value"
        //
        // }
    ]

    // 可以禁用storage来辅助调试,不需要每次都去傻啦吧唧的删除,让它写不进去读不出来即可
    // 可以这样同时控制localStorage和sessionStorage是否可读和可写
    const enableStorage = {
        read: true,
        write: true
    }

    // 支持的另一种配置方式:
    // 也可以精确的为每一个类型指定可读和可写
    // const enableStorage = {
    //     localStorage: {
    //         // localStorage是否是可读的
    //         read: true,
    //         // localStorage是否是可写的
    //         write: true
    //     },
    //     sessionStorage: {
    //         // sessionStorage是否是可读的
    //         read: true,
    //         // sessionStorage是否是可写的
    //         write: true
    //     }
    // }

    // 在控制台打印日志时字体大小,根据自己喜好调整
    // 众所周知,12px是宇宙通用大小
    const consoleLogFontSize = 12;

    // --------------------------------- 以下为程序内部逻辑,可忽略 ---------------------------------------------

    // 防止重复注入
    const _cc11001100_hook_storage = window._cc11001100_hook_storage = window._cc11001100_hook_storage || {};
    if ("isInjectHook" in _cc11001100_hook_storage) {
        return
    }
    _cc11001100_hook_storage["isInjectHook"] = true

    addHook("session", window.sessionStorage);
    addHook("local", window.localStorage);

    /**
     * 为一个storage对象添加Hook,可以是localStorage或者sessionStorage
     *
     * @param storageTypeName { "local" | "session" }
     * @param storageObject { window.localStorage | window.sessionStorage}
     */
    function addHook(storageTypeName, storageObject) {

        // getItem
        const storageGetItem = storageObject.getItem;
        storageObject.getItem = function (itemName) {
            const itemValue = storageGetItem.apply(this, [itemName]);

            const valueStyle = `color: black; background: #85C1E9; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
            const normalStyle = `color: black; background: #D6EAF8; font-size: ${consoleLogFontSize}px;`;

            const message = [

                normalStyle,
                now(),

                normalStyle,
                "Storage Monitor: ",

                valueStyle,
                "get",

                normalStyle,
                " ",

                valueStyle,
                `${storageTypeName} storage`,

                normalStyle,
                ", name = ",

                valueStyle,
                `${itemName}`,

                normalStyle,
                ", value = ",

                valueStyle,
                `${itemValue}`,

                normalStyle,
                `, code location = ${cc11001100_getCodeLocation()}`
            ];
            console.log(genFormatArray(message), ...message);

            testStorageDebugger(storageTypeName, "get", itemName, itemValue);

            // 如果关闭读功能的话,则阻止其能够读到值
            if (!isStorageEnable(storageTypeName, "read")) {
                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    normalStyle,
                    "ignore ",

                    valueStyle,
                    "get",

                    normalStyle,
                    ` because disable `,

                    valueStyle,
                    `${storageTypeName}`,

                    normalStyle,
                    " ",

                    valueStyle,
                    "read",

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
                return null;
            }



            return itemValue;
        }
        storageObject.getItem.toString = () => "function getItem() { [native code] }";

        // setItem
        const storageSetItem = storageObject.setItem;
        storageObject.setItem = function (itemName, itemValue) {

            const oldValue = storageGetItem.apply(this, [itemName]);

            let valueStyle = "";
            let normalStyle = "";

            if (oldValue == null) {
                // 认为是新增
                valueStyle = `color: black; background: #669934; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
                normalStyle = `color: black; background: #65CC66; font-size: ${consoleLogFontSize}px;`;

                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    valueStyle,
                    "set",

                    normalStyle,
                    " ",

                    valueStyle,
                    `${storageTypeName} storage`,

                    normalStyle,
                    ", name = ",

                    valueStyle,
                    `${itemName}`,

                    normalStyle,
                    ", value = ",

                    valueStyle,
                    `${itemValue}`,

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
            } else {
                // 认为是修改
                valueStyle = `color: black; background: #FE9900; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
                normalStyle = `color: black; background: #FFCC00; font-size: ${consoleLogFontSize}px;`;

                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    valueStyle,
                    "set",

                    normalStyle,
                    " ",

                    valueStyle,
                    `${storageTypeName} storage`,

                    normalStyle,
                    ", name = ",

                    valueStyle,
                    `${itemName}`,

                    normalStyle,
                    ", newValue = ",

                    valueStyle,
                    `${itemValue}`,

                    ...(() => {
                        if (oldValue === itemValue) {
                            // 值没有发生改变
                            return [
                                normalStyle,
                                ", value changed = ",

                                valueStyle,
                                `false`
                            ]
                        } else {
                            // 值发生了改变
                            return [
                                normalStyle,
                                ", oldValue = ",

                                valueStyle,
                                `${oldValue}`,

                                normalStyle,
                                ", value changed = ",

                                valueStyle,
                                `true`
                            ]
                        }
                    })(),

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
            }

            testStorageDebugger(storageTypeName, "set", itemName, itemValue);

            // 如果关闭写功能的话,则阻止其能够修改值
            if (!isStorageEnable(storageTypeName, "write")) {
                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    normalStyle,
                    "ignore ",

                    valueStyle,
                    "set",

                    normalStyle,
                    ` because disable `,

                    valueStyle,
                    `${storageTypeName}`,

                    normalStyle,
                    " ",

                    valueStyle,
                    "write",

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
                return null;
            }

            return storageSetItem.apply(this, [itemName, itemValue]);
        }
        storageObject.setItem.toString = () => "function setItem() { [native code] }";

        // removeItem
        const storageRemoveItem = storageObject.removeItem;
        storageObject.removeItem = function (itemName) {

            const oldValue = storageGetItem.apply(this, [itemName]);

            const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
            const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`;

            const message = [

                normalStyle,
                now(),

                normalStyle,
                "Storage Monitor: ",

                valueStyle,
                "remove",

                normalStyle,
                " ",

                valueStyle,
                `${storageTypeName} storage`,

                normalStyle,
                ", name = ",

                valueStyle,
                `${itemName}`,

                normalStyle,
                ", value = ",

                valueStyle,
                `${oldValue}`,

                normalStyle,
                `, code location = ${cc11001100_getCodeLocation()}`
            ];
            console.log(genFormatArray(message), ...message);

            testStorageDebugger(storageTypeName, "remove", itemName, null);

            // 如果关闭写功能的话,则阻止其能够修改值
            if (!isStorageEnable(storageTypeName, "write")) {
                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    normalStyle,
                    "ignore ",

                    valueStyle,
                    "remove",

                    normalStyle,
                    ` because disable `,

                    valueStyle,
                    `${storageTypeName}`,

                    normalStyle,
                    " ",

                    valueStyle,
                    "write",

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
                return null;
            }

            return storageRemoveItem.apply(this, [itemName]);
        }
        storageObject.removeItem.toString = () => "function removeItem() { [native code] }";

        // clear
        const storageClear = storageObject.clear;
        storageObject.clear = function () {

            const valueStyle = `color: black; background: #E50000; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
            const normalStyle = `color: black; background: #FF6766; font-size: ${consoleLogFontSize}px;`;

            const message = [

                normalStyle,
                now(),

                normalStyle,
                "Storage Monitor: ",

                valueStyle,
                "clear",

                normalStyle,
                " ",

                valueStyle,
                `${storageTypeName} storage`,

                normalStyle,
                `, code location = ${cc11001100_getCodeLocation()}`
            ];
            console.log(genFormatArray(message), ...message);

            testStorageDebugger(storageTypeName, "clear", null, null);

            // 如果关闭写功能的话,则阻止其能够修改值
            if (!isStorageEnable(storageTypeName, "write")) {
                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    normalStyle,
                    "ignore ",

                    valueStyle,
                    "clear",

                    normalStyle,
                    ` because disable `,

                    valueStyle,
                    `${storageTypeName}`,

                    normalStyle,
                    " ",

                    valueStyle,
                    "write",

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
                return null;
            }

            return storageClear.apply(this);
        }
        storageObject.clear.toString = () => "function clear() { [native code] }";

        // key
        const storageKey = storageObject.key;
        storageObject.key = function (itemIndex) {
            const value = storageKey.apply(this, [itemIndex]);

            const valueStyle = `color: black; background: #85C1E9; font-size: ${consoleLogFontSize}px; font-weight: bold;`;
            const normalStyle = `color: black; background: #D6EAF8; font-size: ${consoleLogFontSize}px;`;

            const message = [

                normalStyle,
                now(),

                normalStyle,
                "Storage Monitor: ",

                valueStyle,
                `key`,

                normalStyle,
                " ",

                valueStyle,
                `${storageTypeName} storage`,

                normalStyle,
                `, itemIndex = `,

                valueStyle,
                `${itemIndex}`,

                normalStyle,
                ", value = ",

                valueStyle,
                `${value}`,

                normalStyle,
                `, code location = ${cc11001100_getCodeLocation()}`
            ];
            console.log(genFormatArray(message), ...message);

            testStorageDebugger(storageTypeName, "key", null, value);

            // 如果关闭读功能的话,则阻止其能够读到值
            if (!isStorageEnable(storageTypeName, "read")) {
                const message = [

                    normalStyle,
                    now(),

                    normalStyle,
                    "Storage Monitor: ",

                    normalStyle,
                    "ignore ",

                    valueStyle,
                    "key",

                    normalStyle,
                    ` because disable `,

                    valueStyle,
                    `${storageTypeName}`,

                    normalStyle,
                    " ",

                    valueStyle,
                    "read",

                    normalStyle,
                    `, code location = ${cc11001100_getCodeLocation()}`
                ];
                console.log(genFormatArray(message), ...message);
                return null;
            }

            return value;
        }
        storageObject.key.toString = () => "function key() { [native code] }";

    }

    /**
     * 对应类型的storage是否开启
     *
     * @param storageTypeName { "local" | "session" }
     * @param operationType { "read" | "write" }
     */
    function isStorageEnable(storageTypeName, operationType) {
        if (storageTypeName === "local") {
            return enableStorage["localStorage"][operationType]
        } else if (storageTypeName === "session") {
            return enableStorage["sessionStorage"][operationType]
        } else {
            return true
        }
    }

    /**
     * 测试是否要进入断点
     *
     * @param storageType { "local" | "session" | "all" }
     * @param operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
     * @param name { "string" | null }
     * @param value { "string" | null }
     */
    function testStorageDebugger(storageType, operationType, name, value) {
        for (let storageDebugger of storageDebuggerList) {
            // 将鼠标移动到这里在变量上悬停查看其值,能够知道是命中了什么规则
            if (storageDebugger.testDebugger(storageType, operationType, name, value)) {
                debugger;
            }
        }
    }

    // 断点规则
    class StorageDebugger {

        /**
         *
         * @param storageType { "local" | "session" | "all" }
         * @param operationType { "get" | "set" | "remove" | "clear" | "key" | "all" }
         * @param nameFilter { "string" | RegExp | null }
         * @param valueFilter { "string" | RegExp | null }
         */
        constructor(storageType, operationType, nameFilter, valueFilter) {
            this.storageType = storageType;
            this.operationType = operationType;
            this.nameFilter = nameFilter;
            this.valueFilter = valueFilter;
        }

        testDebugger(storageType, operationType, name, value) {
            if (!this.testByStorageType(storageType)) {
                return false
            }
            if (!this.testByOperationType(operationType)) {
                return false
            }
            if (this.nameFilter && !this.testByName(name)) {
                return false;
            }
            if (this.valueFilter && !this.testByValue(value)) {
                return false;
            }
            return true;
        }

        testByStorageType(storageType) {
            if (storageType === "all" || this.storageType === "all") {
                return true
            }
            return this.storageType === storageType;
        }

        testByOperationType(operationType) {
            if (operationType === "all" || this.operationType === "all") {
                return true
            }
            return this.operationType === operationType;
        }

        testByName(name) {

            if (!this.nameFilter) {
                return false
            }

            if (!name) {
                return false
            }

            if (typeof this.nameFilter === "string") {
                return this.nameFilter === name;
            } else if (typeof this.nameFilter instanceof RegExp) {
                return this.nameFilter.test(name)
            } else {
                return false;
            }
        }

        testByValue(value) {

            if (!this.valueFilter) {
                return false
            }

            if (!value) {
                return false
            }

            if (typeof this.valueFilter === "string") {
                return this.valueFilter === value;
            } else if (typeof this.valueFilter instanceof RegExp) {
                return this.valueFilter.test(value)
            } else {
                return false;
            }
        }

    }

    // 把storage的读写属性统一,方便后面程序处理
    (function convertEnableStorage() {

        // 设置默认值
        enableStorage["localStorage"] = enableStorage["localStorage"] || {}
        enableStorage["sessionStorage"] = enableStorage["sessionStorage"] || {}

        // 扩展read
        if ("read" in enableStorage) {
            enableStorage["localStorage"]["read"] = enableStorage["read"]
            enableStorage["sessionStorage"]["read"] = enableStorage["read"]
            delete enableStorage["read"]
        }

        // 扩展write
        if ("write" in enableStorage) {
            enableStorage["localStorage"]["write"] = enableStorage["write"]
            enableStorage["sessionStorage"]["write"] = enableStorage["write"]
            delete enableStorage["write"]
        }

        // 如果没有配置的话,则设置默认值
        if (!("write" in enableStorage["localStorage"])) {
            enableStorage["localStorage"]["write"] = true
        }
        if (!("read" in enableStorage["localStorage"])) {
            enableStorage["localStorage"]["read"] = true
        }
        if (!("write" in enableStorage["sessionStorage"])) {
            enableStorage["sessionStorage"]["write"] = true
        }
        if (!("read" in enableStorage["sessionStorage"])) {
            enableStorage["sessionStorage"]["read"] = true
        }

    })();

    // 把storage的断点规则转换为程序内部使用的格式
    (function convertStorageDebugger() {
        // const valueStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px; font-weight: bold;`;
        const normalStyle = `color: black; background: #FF2121; font-size: ${Math.round(consoleLogFontSize * 1.5)}px;`;

        const newStorageDebuggerList = [];
        for (let x of storageDebuggerList) {
            if (typeof x === "string" || x instanceof RegExp) {
                // 如果设置的是名字,则只针对按名称操作的操作打断点
                newStorageDebuggerList.push(new StorageDebugger("all", "get", x, null));
                newStorageDebuggerList.push(new StorageDebugger("all", "set", x, null));
                newStorageDebuggerList.push(new StorageDebugger("all", "remove", x, null));
            } else {

                // 检查设置项的合法性
                if ("storageType" in x && ["local", "session", "all"].indexOf(x["storageType"].toLowerCase()) === -1) {
                    const message = [
                        normalStyle,
                        `${now()} Storage Monitor: storageType error, value = ${x["storageType"]}, need to be = { "local", "session", "all" }, so ignore this debugger = ${JSON.stringify(x)}`,
                    ];
                    console.log(genFormatArray(message), ...message);
                    continue
                }

                if ("operationType" in x && ["get", "set", "remove", "clear", "key", "all"].indexOf(x["operationType"].toLowerCase()) === -1) {
                    const message = [
                        normalStyle,
                        `${now()} Storage Monitor: storageType error, value = ${x["operationType"]}, need to be { "get" | "set" | "remove" | "clear" | "key" | "all" }, so ignore this debugger = ${JSON.stringify(x)}`,
                    ];
                    console.log(genFormatArray(message), ...message);
                    continue
                }

                if (["nameFilter"] in x && (typeof x["nameFilter"] != "string") && !(x["nameFilter"] instanceof RegExp)) {
                    const message = [
                        normalStyle,
                        `${now()} Storage Monitor: nameFilter config error, value = ${x["nameFilter"]}, need to be { string | Regexp | null }, so ignore this debugger = ${JSON.stringify(x)}`,
                    ];
                    console.log(genFormatArray(message), ...message);
                    continue
                }

                if (["valueFilter"] in x && (typeof x["valueFilter"] != "string") && !(x["valueFilter"] instanceof RegExp)) {
                    const message = [
                        normalStyle,
                        `${now()} Storage Monitor: valueFilter config error, value = ${x["valueFilter"]}, need to be { string | Regexp | null }, so ignore this debugger = ${JSON.stringify(x)}`,
                    ];
                    console.log(genFormatArray(message), ...message);
                    continue
                }

                // TODO 出现了其它类型的key,是否配置错误呢?

                const storageType = x["storageType"] || "all";

                if ((x["name"] || x["value"]) && x["operationType"]) {
                    const name = x["name"] || null;
                    const value = x["value"] || null;
                    newStorageDebuggerList.push(new StorageDebugger("all", "get", name, value));
                    newStorageDebuggerList.push(new StorageDebugger("all", "set", name, value));
                    newStorageDebuggerList.push(new StorageDebugger("all", "remove", name, value));
                } else {
                    const operationType = x["operationType"] || "all";
                    const name = x["name"] || null;
                    const value = x["value"] || null;
                    newStorageDebuggerList.push(new StorageDebugger(storageType, operationType, name, value));
                }
            }
        }

        // 把原来的规则替换掉
        while (storageDebuggerList.pop()) {
        }
        for (let x of newStorageDebuggerList) {
            storageDebuggerList.push(x);
        }
    })();

    // 奇奇怪怪的模板方式竟然一路被沿用下来...(*/ω\*)
    function genFormatArray(messageAndStyleArray) {
        const formatArray = [];
        for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) {
            formatArray.push("%c%s");
        }
        return formatArray.join("");
    }

    function now() {
        // 东八区专属...
        return "[" + new Date(new Date().getTime() + 1000 * 60 * 60 * 8).toJSON().replace("T", " ").replace("Z", "") + "] ";
    }

    function cc11001100_getCodeLocation() {
        const callstack = new Error().stack.split("\n");
        while (callstack.length && callstack[0].indexOf("cc11001100_getCodeLocation") === -1) {
            callstack.shift();
        }
        callstack.shift();
        callstack.shift();

        return callstack[0].trim();
    }

})();