ConsoleHook

utils of hook javascript function and value changes for js reverse engineering

Ekde 2025/01/22. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ConsoleHook
// @namespace    http://tampermonkey.net/
// @version      2025-01-22
// @description  utils of hook javascript function and value changes for js reverse engineering
// @author       @Esonhugh
// @match        http://*
// @match        https://*
// @include        http://*
// @include        https://*
// @icon         https://blog.eson.ninja/img/reol.png
// @grant        none
// @license     MIT
// @run-at document-start
// ==/UserScript==

(function () {
  console.hooks = {
    // settings
    settings: {
      // trigger debugger if hook is caught
      autoDebug: false,
      // don't let page jump to other place
      blockPageJump: false,
      // log prefix
      prefix: "[EHOOKS] ", // u can filter all this things with this tag
      // init with eventListener added
      checkEventListnerAdded: false,
      // init with cookie change listener
      checkCookieChange: false,
      // init with localstorage get set
      checkLocalStorageGetSet: false,
      // anti dead loop debugger in script
      antiDeadLoopDebugger: true,

      // hidden too many default debug logs if you don't need it
      hiddenlog: false,
    },

    rawlog: function (...data) {
      if (this.settings.hiddenlog) {
        return; // don't print
      }
      return console.debug(...data);
    },

    log: console.warn,

    debugger: function () {
      // traped in debug
      if (this.settings.autoDebug) {
        // dump the real stack for u
        this.dumpstack();
        debugger;
      }
    },

    hooked: {},

    dumpstack(print = true) {
      var err = new Error();
      var stack = err.stack.split("\n");
      var ret = [`${this.settings.prefix}DUMP STACK: `];
      for (var i of stack) {
        if (!i.includes("userscript.html") && i !== "Error") {
          ret = ret.concat(i);
        }
      }
      ret = ret.join("\n");
      if (print) {
        this.log(ret);
      }
      return ret;
    },

    dumpHooked() {
      for (var i in this.hooked) {
        if (this.hooked[i].toString) {
          this.log(`${i}: ${this.hooked[i].toString()}`);
        } else {
          this.log(`${i}: ${this.hooked[i]}`);
        }
      }
    },

    hookfunc: function (
      object,
      functionName,
      posthook = () => {},
      prehook = () => {},
      slience = false
    ) {
      (function (originalFunction) {
        object[functionName] = function () {
          // hook logic
          // 1. Allow Check
          var args = prehook([originalFunction, arguments, this]);
          var realargs = arguments;
          if (args) {
            realargs = args;
          } else {
            realargs = arguments;
          }
          // 2. Execute old function
          var returnValue = originalFunction.apply(this, realargs);
          if (!slience) { // not slience
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Hook function trap-> func[${functionName}]`,
              "args->",
              realargs,
              "ret->",
              returnValue
            );
            console.hooks.debugger();
          }
          // 3. Post hook change values
          var newReturn = posthook([
            returnValue,
            originalFunction,
            realargs,
            this,
          ]);
          if (newReturn) {
            return newReturn;
          }
          return returnValue;
        };
        object[functionName].toString = function () {
          console.hooks.log(
            `${console.hooks.settings.prefix}Found hook toString check!`,
            originalFunction
          );
          console.hooks.debugger();
          return originalFunction.toString();
        };
        console.hooks.hooked[functionName] = originalFunction;
      })(object[functionName]);
      this.log(
        `${console.hooks.settings.prefix}Hook function`,
        functionName,
        "success!"
      );
    },

    unhookfunc: function (object, functionName) {
      object[functionName] = console.hooks.hooked[functionName];
      this.rawlog(
        `${console.hooks.settings.prefix}unHook function`,
        functionName,
        "success!"
      );
    },

    hookCookie: function () {
      try {
        var cookieDesc =
          Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ||
          Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie");
        if (cookieDesc && cookieDesc.configurable) {
          this.hooked["Cookie"] = document.cookie;
          Object.defineProperty(document, "cookie", {
            set: function (val) {
              console.hooks.rawlog(
                `${console.hooks.settings.prefix}Hook捕获到cookie设置->`,
                val
              );
              console.hooks.debugger();
              console.hooks.hooked["Cookie"] = val;
              return val;
            },
            get: function () {
              return (console.hooks.hooked["Cookie"] = "");
            },
            configurable: true,
          });
        } else {
          var org = document.__lookupSetter__("cookie");
          document.__defineSetter__("cookie", function (cookie) {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Cookie Set as`,
              cookie
            );
            console.hooks.debugger();
            org = cookie;
          });
          document.__defineGetter__("cookie", function () {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Cookie Got`,
              org
            );
            console.hooks.debugger();
            return org;
          });
        }
      } catch (e) {
        this.rawlog(`${console.hooks.settings.prefix}Cookie hook failed!`);
      }
    },

    hookLocalStorage: function () {
      this.hookfunc(localStorage, "getItem");
      this.hookfunc(localStorage, "setItem");
      this.hookfunc(localStorage, "removeItem");
      this.hookfunc(localStorage, "clear");
      this.rawlog(`${console.hooks.settings.prefix}LocalStorage hooked!`);
    },

    hookValueViaGetSet: function (name, obj, key) {
      if (obj[key]) {
        this.hooked[key] = obj[key];
      }
      var obj_name = `OBJ_${name}.${key}`;
      var org = obj.__lookupSetter__(key);
      obj.__defineSetter__(key, function (val) {
        org = console.hooks.hooked[key];
        console.hooks.rawlog(
          `${console.hooks.settings.prefix}Hook value set `,
          obj_name,
          "value->",
          org,
          "newvalue->",
          val
        );
        console.hooks.debugger();
        console.hooks.hooked[key] = val;
      });
      obj.__defineGetter__(key, function () {
        org = console.hooks.hooked[key];
        console.hooks.rawlog(
          `${console.hooks.settings.prefix}Hook value get `,
          obj_name,
          "value->",
          org
        );
        console.hooks.debugger();
        return org;
      });
    },

    GetSetter(obj_name, key) {
      return {
        get: function (target, property, receiver) {
          var ret = target[property];
          if (key === "default_all") {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Hook Proxy value get`,
              `${obj_name}.${property}`,
              "value->",
              ret
            );
            console.hooks.debugger();
          }
          if (property == key && key != "default_all") {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Hook Proxy value get`,
              `${obj_name}.${property}`,
              "value->",
              ret
            );
            console.hooks.debugger();
          }
          return target[property];
        },
        set: function (target, property, newValue, receiver) {
          var ret = target[property];
          if (key === "default_all") {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Hook Proxy value set`,
              `${obj_name}.${property}`,
              "value->",
              ret,
              "newvalue->",
              newValue
            );
            console.hooks.debugger();
          }
          if (property == key && key != "default_all") {
            console.hooks.rawlog(
              `${console.hooks.settings.prefix}Hook Proxy value get`,
              `${obj_name}.${property}`,
              "value->",
              ret,
              "newvalue->",
              newValue
            );
            console.hooks.debugger();
          }
          target[property] = newValue;
          return true;
        },
      };
    },

    hookValueViaProxy: function (name, obj, key = "default_all") {
      var obj_name = "OBJ_" + name;
      return this.utils.createProxy(obj, this.GetSetter(obj_name, key));
    },

    hookValueViaObject: function (name, obj, key) {
      var obj_desc = Object.getOwnPropertyDescriptor(obj, key);
      if (!obj_desc || !obj_desc.configurable || obj[key] === undefined) {
        return Error("No Priv to set Property or No such keys!");
      }
      var obj_name = "OBJ_" + name;
      this.hooked[obj_name] = obj[key];
      Object.defineProperty(obj, key, {
        configurable: true,
        get() {
          console.hooks.rawlog(
            `${console.hooks.settings.prefix}Hook Object value get`,
            `${obj_name}.${key}`,
            "value->",
            console.hooks.hooked[obj_name]
          );
          console.hooks.debugger();
          return console.hooks.hooked[obj_name];
        },
        set(v) {
          console.hooks.rawlog(
            `${console.hooks.settings.prefix}Hook Proxy value get`,
            `${obj_name}.${key}`,
            "value->",
            console.hooks.hooked[obj_name],
            "newvalue->",
            v
          );
          console.hooks.hooked[obj_name] = v;
        },
      });
    },

    hookEvents: function (params) {
      var placeToReplace;
      if (window.EventTarget && EventTarget.prototype.addEventListener) {
        placeToReplace = EventTarget;
      } else {
        placeToReplace = Element;
      }
      this.hookfunc(
        placeToReplace.prototype,
        "addEventListener",
        function (res) {
          let [ret, originalFunction, arguments] = res;
          console.hooks.rawlog(
            `${console.hooks.settings.prefix}Hook event listener added!`,
            arguments
          );
        }
      );
    },

    antiDebuggerLoops: function () {
      processDebugger = (type, res) => {
        let [originalFunction, arguments, t] = res;
        var handler = arguments[0];
        console.hooks.debugger();
        if (handler.toString().includes("debugger")) {
          console.hooks.log(
            `${console.hooks.settings.prefix}found debug loop in ${type}`
          );
          console.hooks.debugger();
          let func = handler.toString().replaceAll("debugger", "");
          arguments[0] = new Function("return " + func)();
          return arguments;
        } else {
          return arguments;
        }
      };

      this.hookfunc(
        window,
        "setInterval",
        () => {},
        (res) => {
          return processDebugger("setInterval", res);
        }, true
      );
      this.hookfunc(
        window,
        "setTimeout",
        () => {},
        (res) => {
          return processDebugger("setTimeout", res);
        }, true
      );

      this.hookfunc(Function.prototype, "constructor", (res) => {
        let [ret, originalFunction, arguments, env] = res;
        if (ret.toString().includes("debugger")) {
          console.hooks.log(
            `${console.hooks.settings.prefix}found debug loop in Function constructor`
          );
          console.hooks.debugger();
          let func = ret.toString().replaceAll("debugger", "");
          return new Function("return " + func)();
        }
        return ret;
      });
    },

    init: function () {
      if (this.utils)
        if (this.settings.blockPageJump) {
          window.onbeforeunload = function () {
            return "ANTI LEAVE";
          };
        }
      if (this.settings.checkEventListnerAdded) {
        this.hookEvents();
      }
      if (this.settings.checkCookieChange) {
        this.hookCookie();
      }
      if (this.settings.checkLocalStorageGetSet) {
        this.hookLocalStorage();
      }
      if (this.settings.antiDeadLoopDebugger) {
        this.antiDebuggerLoops();
      }
    },

    main: function () {
      if (!this.settings.antiDeadLoopDebugger) {
        this.hookfunc(window, "setInterval");
        this.hookfunc(window, "setTimeout");
        this.hookfunc(Function.prototype, "constructor");
      }
      this.hookfunc(window, "eval");
      this.hookfunc(window, "Function");
      this.hookfunc(window, "atob");
      this.hookfunc(window, "btoa");
      this.hookfunc(window, "fetch");
      this.hookfunc(window, "encodeURI");
      this.hookfunc(window, "encodeURIComponent");

      this.hookfunc(JSON, "parse");
      this.hookfunc(JSON, "stringify");

      this.hookfunc(console, "log");
      // this.hookfunc(console, "warn")
      // this.hookfunc(console, "error")
      // this.hookfunc(console, "info")
      // this.hookfunc(console, "debug")
      // this.hookfunc(console, "table")
      // this.hookfunc(console, "trace")
      this.hookfunc(console, "clear");
    },
  };

  // Console Hooks utils for
  {
    console.hooks.utils = {};

    console.hooks.utils.init = () => {
      console.hooks.utils.preloadCache();
    };

    /**
     * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw.
     *
     * The presence of a JS Proxy can be revealed as it shows up in error stack traces.
     *
     * @param {object} handler - The JS Proxy handler to wrap
     */
    console.hooks.utils.stripProxyFromErrors = (handler = {}) => {
      const newHandler = {
        setPrototypeOf: function (target, proto) {
          if (proto === null)
            throw new TypeError("Cannot convert object to primitive value");
          if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {
            throw new TypeError("Cyclic __proto__ value");
          }
          return Reflect.setPrototypeOf(target, proto);
        },
      };
      // We wrap each trap in the handler in a try/catch and modify the error stack if they throw
      const traps = Object.getOwnPropertyNames(handler);
      traps.forEach((trap) => {
        newHandler[trap] = function () {
          try {
            // Forward the call to the defined proxy handler
            return handler[trap].apply(this, arguments || []);
          } catch (err) {
            // Stack traces differ per browser, we only support chromium based ones currently
            if (!err || !err.stack || !err.stack.includes(`at `)) {
              throw err;
            }

            // When something throws within one of our traps the Proxy will show up in error stacks
            // An earlier implementation of this code would simply strip lines with a blacklist,
            // but it makes sense to be more surgical here and only remove lines related to our Proxy.
            // We try to use a known "anchor" line for that and strip it with everything above it.
            // If the anchor line cannot be found for some reason we fall back to our blacklist approach.

            const stripWithBlacklist = (stack, stripFirstLine = true) => {
              const blacklist = [
                `at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply
                `at Object.${trap} `, // e.g. Object.get or Object.apply
                `at Object.newHandler.<computed> [as ${trap}] `, // caused by this very wrapper :-)
              ];
              return (
                err.stack
                  .split("\n")
                  // Always remove the first (file) line in the stack (guaranteed to be our proxy)
                  .filter((line, index) => !(index === 1 && stripFirstLine))
                  // Check if the line starts with one of our blacklisted strings
                  .filter(
                    (line) =>
                      !blacklist.some((bl) => line.trim().startsWith(bl))
                  )
                  .join("\n")
              );
            };

            const stripWithAnchor = (stack, anchor) => {
              const stackArr = stack.split("\n");
              anchor =
                anchor || `at Object.newHandler.<computed> [as ${trap}] `; // Known first Proxy line in chromium
              const anchorIndex = stackArr.findIndex((line) =>
                line.trim().startsWith(anchor)
              );
              if (anchorIndex === -1) {
                return false; // 404, anchor not found
              }
              // Strip everything from the top until we reach the anchor line
              // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
              stackArr.splice(1, anchorIndex);
              return stackArr.join("\n");
            };

            // Special cases due to our nested toString proxies
            err.stack = err.stack.replace(
              "at Object.toString (",
              "at Function.toString ("
            );
            if ((err.stack || "").includes("at Function.toString (")) {
              err.stack = stripWithBlacklist(err.stack, false);
              throw err;
            }

            // Try using the anchor method, fallback to blacklist if necessary
            err.stack =
              stripWithAnchor(err.stack) || stripWithBlacklist(err.stack);

            throw err; // Re-throw our now sanitized error
          }
        };
      });
      return newHandler;
    };

    /**
     * Strip error lines from stack traces until (and including) a known line the stack.
     *
     * @param {object} err - The error to sanitize
     * @param {string} anchor - The string the anchor line starts with
     */
    console.hooks.utils.stripErrorWithAnchor = (err, anchor) => {
      const stackArr = err.stack.split("\n");
      const anchorIndex = stackArr.findIndex((line) =>
        line.trim().startsWith(anchor)
      );
      if (anchorIndex === -1) {
        return err; // 404, anchor not found
      }
      // Strip everything from the top until we reach the anchor line (remove anchor line as well)
      // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
      stackArr.splice(1, anchorIndex);
      err.stack = stackArr.join("\n");
      return err;
    };

    /**
     * Replace the property of an object in a stealthy way.
     *
     * Note: You also want to work on the prototype of an object most often,
     * as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)).
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
     *
     * @example
     * replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" })
     * // or
     * replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] })
     *
     * @param {object} obj - The object which has the property to replace
     * @param {string} propName - The property name to replace
     * @param {object} descriptorOverrides - e.g. { value: "alice" }
     */
    console.hooks.utils.replaceProperty = (
      obj,
      propName,
      descriptorOverrides = {}
    ) => {
      return Object.defineProperty(obj, propName, {
        // Copy over the existing descriptors (writable, enumerable, configurable, etc)
        ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),
        // Add our overrides (e.g. value, get())
        ...descriptorOverrides,
      });
    };

    /**
     * Preload a cache of function copies and data.
     *
     * For a determined enough observer it would be possible to overwrite and sniff usage of functions
     * we use in our internal Proxies, to combat that we use a cached copy of those functions.
     *
     * Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before,
     * by executing `console.hooks.utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups).
     *
     * This is evaluated once per execution context (e.g. window)
     */
    console.hooks.utils.preloadCache = () => {
      if (console.hooks.utils.cache) {
        return;
      }
      console.hooks.utils.cache = {
        // Used in our proxies
        Reflect: {
          get: Reflect.get.bind(Reflect),
          apply: Reflect.apply.bind(Reflect),
        },
        // Used in `makeNativeString`
        nativeToStringStr: Function.toString + "", // => `function toString() { [native code] }`
      };
    };

    /**
     * Utility function to generate a cross-browser `toString` result representing native code.
     *
     * There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings.
     * To future-proof this we use an existing native toString result as the basis.
     *
     * The only advantage we have over the other team is that our JS runs first, hence we cache the result
     * of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it.
     *
     * @example
     * makeNativeString('foobar') // => `function foobar() { [native code] }`
     *
     * @param {string} [name] - Optional function name
     */
    console.hooks.utils.makeNativeString = (name = "") => {
      return console.hooks.utils.cache.nativeToStringStr.replace(
        "toString",
        name || ""
      );
    };

    /**
     * Helper function to modify the `toString()` result of the provided object.
     *
     * Note: Use `console.hooks.utils.redirectToString` instead when possible.
     *
     * There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object.
     * If no string is provided we will generate a `[native code]` thing based on the name of the property object.
     *
     * @example
     * patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }')
     *
     * @param {object} obj - The object for which to modify the `toString()` representation
     * @param {string} str - Optional string used as a return value
     */
    console.hooks.utils.patchToString = (obj, str = "") => {
      const handler = {
        apply: function (target, ctx) {
          // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
          if (ctx === Function.prototype.toString) {
            return console.hooks.utils.makeNativeString("toString");
          }
          // `toString` targeted at our proxied Object detected
          if (ctx === obj) {
            // We either return the optional string verbatim or derive the most desired result automatically
            return str || console.hooks.utils.makeNativeString(obj.name);
          }
          // Check if the toString protype of the context is the same as the global prototype,
          // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
          const hasSameProto = Object.getPrototypeOf(
            Function.prototype.toString
          ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
          if (!hasSameProto) {
            // Pass the call on to the local Function.prototype.toString instead
            return ctx.toString();
          }
          return target.call(ctx);
        },
      };

      const toStringProxy = new Proxy(
        Function.prototype.toString,
        console.hooks.utils.stripProxyFromErrors(handler)
      );
      console.hooks.utils.replaceProperty(Function.prototype, "toString", {
        value: toStringProxy,
      });
    };

    /**
     * Make all nested functions of an object native.
     *
     * @param {object} obj
     */
    console.hooks.utils.patchToStringNested = (obj = {}) => {
      return console.hooks.utils.execRecursively(
        obj,
        ["function"],
        utils.patchToString
      );
    };

    /**
     * Redirect toString requests from one object to another.
     *
     * @param {object} proxyObj - The object that toString will be called on
     * @param {object} originalObj - The object which toString result we wan to return
     */
    console.hooks.utils.redirectToString = (proxyObj, originalObj) => {
      const handler = {
        apply: function (target, ctx) {
          // This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
          if (ctx === Function.prototype.toString) {
            return console.hooks.utils.makeNativeString("toString");
          }

          // `toString` targeted at our proxied Object detected
          if (ctx === proxyObj) {
            const fallback = () =>
              originalObj && originalObj.name
                ? console.hooks.utils.makeNativeString(originalObj.name)
                : console.hooks.utils.makeNativeString(proxyObj.name);

            // Return the toString representation of our original object if possible
            return originalObj + "" || fallback();
          }

          if (typeof ctx === "undefined" || ctx === null) {
            return target.call(ctx);
          }

          // Check if the toString protype of the context is the same as the global prototype,
          // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
          const hasSameProto = Object.getPrototypeOf(
            Function.prototype.toString
          ).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
          if (!hasSameProto) {
            // Pass the call on to the local Function.prototype.toString instead
            return ctx.toString();
          }

          return target.call(ctx);
        },
      };

      const toStringProxy = new Proxy(
        Function.prototype.toString,
        console.hooks.utils.stripProxyFromErrors(handler)
      );
      console.hooks.utils.replaceProperty(Function.prototype, "toString", {
        value: toStringProxy,
      });
    };

    /**
     * All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps.
     *
     * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
     * Note: This is meant to modify native Browser APIs and works best with prototype objects.
     *
     * @example
     * replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler)
     *
     * @param {object} obj - The object which has the property to replace
     * @param {string} propName - The name of the property to replace
     * @param {object} handler - The JS Proxy handler to use
     */
    console.hooks.utils.replaceWithProxy = (obj, propName, handler) => {
      const originalObj = obj[propName];
      const proxyObj = new Proxy(
        obj[propName],
        console.hooks.utils.stripProxyFromErrors(handler)
      );

      console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
      console.hooks.utils.redirectToString(proxyObj, originalObj);

      return true;
    };
    /**
     * All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps.
     *
     * @example
     * replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler)
     *
     * @param {object} obj - The object which has the property to replace
     * @param {string} propName - The name of the property to replace
     * @param {object} handler - The JS Proxy handler to use
     */
    console.hooks.utils.replaceGetterWithProxy = (obj, propName, handler) => {
      const fn = Object.getOwnPropertyDescriptor(obj, propName).get;
      const fnStr = fn.toString(); // special getter function string
      const proxyObj = new Proxy(
        fn,
        console.hooks.utils.stripProxyFromErrors(handler)
      );

      console.hooks.utils.replaceProperty(obj, propName, { get: proxyObj });
      console.hooks.utils.patchToString(proxyObj, fnStr);

      return true;
    };

    /**
     * All-in-one method to replace a getter and/or setter. Functions get and set
     * of handler have one more argument that contains the native function.
     *
     * @example
     * replaceGetterSetter(HTMLIFrameElement.prototype, 'contentWindow', handler)
     *
     * @param {object} obj - The object which has the property to replace
     * @param {string} propName - The name of the property to replace
     * @param {object} handlerGetterSetter - The handler with get and/or set
     *                                     functions
     * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description
     */
    console.hooks.utils.replaceGetterSetter = (
      obj,
      propName,
      handlerGetterSetter
    ) => {
      const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(
        obj,
        propName
      );
      const handler = { ...ownPropertyDescriptor };

      if (handlerGetterSetter.get !== undefined) {
        const nativeFn = ownPropertyDescriptor.get;
        handler.get = function () {
          return handlerGetterSetter.get.call(this, nativeFn.bind(this));
        };
        console.hooks.utils.redirectToString(handler.get, nativeFn);
      }

      if (handlerGetterSetter.set !== undefined) {
        const nativeFn = ownPropertyDescriptor.set;
        handler.set = function (newValue) {
          handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this));
        };
        console.hooks.utils.redirectToString(handler.set, nativeFn);
      }

      Object.defineProperty(obj, propName, handler);
    };

    /**
     * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps.
     *
     * Will stealthify these aspects (strip error stack traces, redirect toString, etc).
     *
     * @example
     * mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler)
     *
     * @param {object} obj - The object which has the property to replace
     * @param {string} propName - The name of the property to replace or create
     * @param {object} pseudoTarget - The JS Proxy target to use as a basis
     * @param {object} handler - The JS Proxy handler to use
     */
    console.hooks.utils.mockWithProxy = (
      obj,
      propName,
      pseudoTarget,
      handler
    ) => {
      const proxyObj = new Proxy(
        pseudoTarget,
        console.hooks.utils.stripProxyFromErrors(handler)
      );

      console.hooks.utils.replaceProperty(obj, propName, { value: proxyObj });
      console.hooks.utils.patchToString(proxyObj);

      return true;
    };

    /**
     * All-in-one method to create a new JS Proxy with stealth tweaks.
     *
     * This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property.
     *
     * Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc).
     *
     * @example
     * createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy
     *
     * @param {object} pseudoTarget - The JS Proxy target to use as a basis
     * @param {object} handler - The JS Proxy handler to use
     */
    console.hooks.utils.createProxy = (pseudoTarget, handler) => {
      const proxyObj = new Proxy(
        pseudoTarget,
        console.hooks.utils.stripProxyFromErrors(handler)
      );
      console.hooks.utils.patchToString(proxyObj);

      return proxyObj;
    };

    /**
     * Helper function to split a full path to an Object into the first part and property.
     *
     * @example
     * splitObjPath(`HTMLMediaElement.prototype.canPlayType`)
     * // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"}
     *
     * @param {string} objPath - The full path to an object as dot notation string
     */
    console.hooks.utils.splitObjPath = (objPath) => ({
      // Remove last dot entry (property) ==> `HTMLMediaElement.prototype`
      objName: objPath.split(".").slice(0, -1).join("."),
      // Extract last dot entry ==> `canPlayType`
      propName: objPath.split(".").slice(-1)[0],
    });

    /**
     * Convenience method to replace a property with a JS Proxy using the provided objPath.
     *
     * Supports a full path (dot notation) to the object as string here, in case that makes it easier.
     *
     * @example
     * replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler)
     *
     * @param {string} objPath - The full path to an object (dot notation string) to replace
     * @param {object} handler - The JS Proxy handler to use
     */
    console.hooks.utils.replaceObjPathWithProxy = (objPath, handler) => {
      const { objName, propName } = console.hooks.utils.splitObjPath(objPath);
      const obj = eval(objName); // eslint-disable-line no-eval
      return console.hooks.utils.replaceWithProxy(obj, propName, handler);
    };

    /**
     * Traverse nested properties of an object recursively and apply the given function on a whitelist of value types.
     *
     * @param {object} obj
     * @param {array} typeFilter - e.g. `['function']`
     * @param {Function} fn - e.g. `console.hooks.utils.patchToString`
     */
    console.hooks.utils.execRecursively = (obj = {}, typeFilter = [], fn) => {
      function recurse(obj) {
        for (const key in obj) {
          if (obj[key] === undefined) {
            continue;
          }
          if (obj[key] && typeof obj[key] === "object") {
            recurse(obj[key]);
          } else {
            if (obj[key] && typeFilter.includes(typeof obj[key])) {
              fn.call(this, obj[key]);
            }
          }
        }
      }
      recurse(obj);
      return obj;
    };

    /**
     * Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one.
     * That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter.
     *
     * Unfortunately the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process.
     * This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings.
     *
     * We use this to pass down our utility functions as well as any other functions (to be able to split up code better).
     *
     * @see console.hooks.utils.materializeFns
     *
     * @param {object} fnObj - An object containing functions as properties
     */
    console.hooks.utils.stringifyFns = (fnObj = { hello: () => "world" }) => {
      // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine
      // https://github.com/feross/fromentries
      function fromEntries(iterable) {
        return [...iterable].reduce((obj, [key, val]) => {
          obj[key] = val;
          return obj;
        }, {});
      }
      return (Object.fromEntries || fromEntries)(
        Object.entries(fnObj)
          .filter(([key, value]) => typeof value === "function")
          .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval
      );
    };

    /**
     * Utility function to reverse the process of `console.hooks.utils.stringifyFns`.
     * Will materialize an object with stringified functions (supports classic and fat arrow functions).
     *
     * @param {object} fnStrObj - An object containing stringified functions as properties
     */
    console.hooks.utils.materializeFns = (
      fnStrObj = { hello: "() => 'world'" }
    ) => {
      return Object.fromEntries(
        Object.entries(fnStrObj).map(([key, value]) => {
          if (value.startsWith("function")) {
            // some trickery is needed to make oldschool functions work :-)
            return [key, eval(`() => ${value}`)()]; // eslint-disable-line no-eval
          } else {
            // arrow functions just work
            return [key, eval(value)]; // eslint-disable-line no-eval
          }
        })
      );
    };

    // Proxy handler templates for re-usability
    console.hooks.utils.makeHandler = () => ({
      // Used by simple `navigator` getter evasions
      getterValue: (value) => ({
        apply(target, ctx, args) {
          // Let's fetch the value first, to trigger and escalate potential errors
          // Illegal invocations like `navigator.__proto__.vendor` will throw here
          console.hooks.utils.cache.Reflect.apply(...arguments);
          return value;
        },
      }),
    });

    /**
     * Compare two arrays.
     *
     * @param {array} array1 - First array
     * @param {array} array2 - Second array
     */
    console.hooks.utils.arrayEquals = (array1, array2) => {
      if (array1.length !== array2.length) {
        return false;
      }
      for (let i = 0; i < array1.length; ++i) {
        if (array1[i] !== array2[i]) {
          return false;
        }
      }
      return true;
    };

    /**
     * Cache the method return according to its arguments.
     *
     * @param {Function} fn - A function that will be cached
     */
    console.hooks.utils.memoize = (fn) => {
      const cache = [];
      return function (...args) {
        if (!cache.some((c) => console.hooks.utils.arrayEquals(c.key, args))) {
          cache.push({ key: args, value: fn.apply(this, args) });
        }
        return cache.find((c) => console.hooks.utils.arrayEquals(c.key, args))
          .value;
      };
    };
  }
  // auto run init
  console.hooks.init();
})();