Neutralizes devtools detection and blocking by libraries like disable-devtool and devtools-detector.
// ==UserScript==
// @name DevTools Unlocked
// @namespace groknt
// @version 3.6.2
// @description Neutralizes devtools detection and blocking by libraries like disable-devtool and devtools-detector.
// @author groknt
// @license MIT
// @match *://*/*
// @match file:///*
// @inject-into auto
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
"use strict";
const BLOCKED_EVENTS = ["contextmenu", "selectstart", "copy", "cut", "paste"];
const DEBUGGER_BODY_RE = /^\s*debugger\s*;?\s*$/;
const DEVTOOL_WARN_RE = /permission.+DEVTOOL/i;
function coreSetup(env) {
let detectorPresent = false;
let recentTrap = false;
let recentTrapTimer = 0;
const onTrapDetected = () => {
detectorPresent = true;
recentTrap = true;
env.clearTimeout(recentTrapTimer);
recentTrapTimer = env.setTimeout(() => {
recentTrap = false;
}, 100);
};
const hasOwn = (obj, prop) =>
env.apply(env.ObjectProto.hasOwnProperty, obj, [prop]);
function isTrap(value, depth) {
if (value == null) return false;
const type = typeof value;
if (type !== "object" && type !== "function") return false;
try {
if (
hasOwn(value, "toString") &&
(env.isDate(value) || env.isRegExp(value) || type === "function")
) {
return true;
}
if (env.isElement(value)) {
const desc = env.desc(value, "id");
if (desc && typeof desc.get === "function") return true;
}
if (env.isArray(value) && value.length >= 50 && value[0] === value[1]) {
const first = value[0];
if (
first != null &&
typeof first === "object" &&
!env.isArray(first) &&
env.keys(first).length >= 100
) {
return true;
}
}
if (
!depth &&
type === "object" &&
!env.isArray(value) &&
!env.isElement(value)
) {
const keys = env.keys(value);
if (keys.length > 0 && keys.length <= 10) {
for (let i = 0; i < keys.length; i++) {
const desc = env.desc(value, keys[i]);
if (desc && "value" in desc && isTrap(desc.value, 1)) return true;
}
}
}
} catch {
return true;
}
return false;
}
function hasTrap(args) {
for (let i = 0; i < args.length; i++) {
if (isTrap(args[i])) return true;
}
return false;
}
const isDetectorNoise = (args) =>
detectorPresent &&
args.length === 1 &&
args[0] != null &&
typeof args[0] === "object" &&
!env.isArray(args[0]) &&
env.getProto(args[0]) === env.ObjectProto &&
env.keys(args[0]).length === 0;
const nativeConsole = {
log: env.console.log,
table: env.console.table,
warn: env.console.warn,
clear: env.console.clear,
dir: env.console.dir,
dirxml: env.console.dirxml,
};
const deferNative = (nativeFn, args) =>
env.setTimeout(() => env.apply(nativeFn, env.console, args), 0);
env.console.log = env.func(function log(...args) {
if (hasTrap(args)) return onTrapDetected();
if (isDetectorNoise(args)) return;
deferNative(nativeConsole.log, args);
}, "log");
env.console.table = env.func(function table(...args) {
if (hasTrap(args)) return onTrapDetected();
if (isDetectorNoise(args)) return;
deferNative(nativeConsole.table, args);
}, "table");
env.console.warn = env.func(function warn(...args) {
if (
args.length &&
typeof args[0] === "string" &&
DEVTOOL_WARN_RE.test(args[0])
) {
detectorPresent = true;
return;
}
deferNative(nativeConsole.warn, args);
}, "warn");
env.console.clear = env.func(function clear() {
if (recentTrap || detectorPresent) return;
deferNative(nativeConsole.clear, []);
}, "clear");
if (nativeConsole.dir) {
env.console.dir = env.func(function dir(...args) {
if (hasTrap(args)) return onTrapDetected();
if (isDetectorNoise(args)) return;
deferNative(nativeConsole.dir, args);
}, "dir");
}
if (nativeConsole.dirxml) {
env.console.dirxml = env.func(function dirxml(...args) {
if (hasTrap(args)) return onTrapDetected();
if (isDetectorNoise(args)) return;
deferNative(nativeConsole.dirxml, args);
}, "dirxml");
}
const isTrapFormatter = (formatter) =>
formatter != null &&
typeof formatter.header === "function" &&
formatter.header.length === 0 &&
typeof formatter.hasBody !== "function";
let realFormatters = env.win.devtoolsFormatters;
try {
env.def(env.win, "devtoolsFormatters", {
configurable: true,
get: env.wrap(() => realFormatters),
set: env.wrap((val) => {
if (env.isArray(val)) {
const clean = env.newArray();
let trapped = false;
for (let i = 0; i < val.length; i++) {
if (isTrapFormatter(val[i])) trapped = true;
else clean.push(val[i]);
}
if (trapped) detectorPresent = true;
val = clean;
const origPush = val.push;
val.push = env.func(function push(...items) {
const cleanItems = env.newArray();
for (let i = 0; i < items.length; i++) {
if (isTrapFormatter(items[i])) detectorPresent = true;
else cleanItems.push(items[i]);
}
return cleanItems.length > 0
? env.apply(origPush, this, cleanItems)
: this.length;
}, "push");
}
realFormatters = val;
}),
});
} catch {}
if (env.patchDescriptor) {
env.patchDescriptor("devtoolsFormatters", () => realFormatters);
}
try {
const outerWidthDesc =
env.desc(env.win, "outerWidth") ||
env.desc(env.WindowProto, "outerWidth");
const outerHeightDesc =
env.desc(env.win, "outerHeight") ||
env.desc(env.WindowProto, "outerHeight");
const getReal = (desc) =>
desc?.get ? env.apply(desc.get, env.win, []) : 0;
env.def(
env.win,
"outerWidth",
env.getter(function outerWidth() {
const real = getReal(outerWidthDesc);
const inner = env.win.innerWidth;
if (real - inner > 100) return Math.round(inner + 16);
return real;
}, "get outerWidth"),
);
env.def(
env.win,
"outerHeight",
env.getter(function outerHeight() {
const real = getReal(outerHeightDesc);
const inner = env.win.innerHeight;
if (real - inner > 100) return Math.round(inner + 16);
return real;
}, "get outerHeight"),
);
} catch {}
function isDevToolKey(e) {
const keyCode = e.keyCode || e.which;
if (keyCode === 123) return true;
const ctrl = e.ctrlKey,
meta = e.metaKey,
shift = e.shiftKey,
alt = e.altKey;
return (
(ctrl && shift && (keyCode === 73 || keyCode === 74)) ||
(meta && alt && (keyCode === 73 || keyCode === 74)) ||
(ctrl && keyCode === 85) ||
(meta && alt && keyCode === 85)
);
}
env.on(
env.win,
"keydown",
env.wrap((e) => {
if (isDevToolKey(e)) env.stop(e);
}),
true,
);
const origFunction = env.win.Function;
const isDebuggerBody = (body) =>
typeof body === "string" && DEBUGGER_BODY_RE.test(body);
const noopFn = env.func(() => {}, "anonymous");
const funcHandler = env.newObject();
funcHandler.apply = env.wrap((target, thisArg, args) => {
if (args.length && isDebuggerBody(args[args.length - 1])) return noopFn;
return env.reflectApply(target, thisArg, args);
});
funcHandler.construct = env.wrap((target, args, newTarget) => {
if (args.length && isDebuggerBody(args[args.length - 1])) return noopFn;
return env.reflectConstruct(target, args, newTarget);
});
const FunctionProxy = env.proxy(origFunction, funcHandler, "Function");
env.win.Function = FunctionProxy;
origFunction.prototype.constructor = FunctionProxy;
const nativeAddListener = env.EventTargetProto.addEventListener;
const nativeRemoveListener = env.EventTargetProto.removeEventListener;
const blockedEvents = env.newSet(env.blockedEvents);
const listenerMap = env.newWeakMap();
const isWindow = (obj) => {
try {
return obj.self === obj;
} catch {
return false;
}
};
env.EventTargetProto.addEventListener = env.func(function addEventListener(
type,
listener,
options,
) {
if (
!blockedEvents.has(type) ||
typeof listener !== "function" ||
!isWindow(this)
) {
return env.apply(nativeAddListener, this, [type, listener, options]);
}
let wrapped = listenerMap.get(listener);
if (!wrapped) {
wrapped = env.wrap(function (e) {
if (!detectorPresent) return env.apply(listener, this, [e]);
const realPreventDefault = e.preventDefault;
env.def(e, "preventDefault", {
value: env.func(() => {}, "preventDefault"),
writable: true,
configurable: true,
});
const returnValueDesc = env.desc(e, "returnValue");
env.def(e, "returnValue", {
get: env.wrap(() => true),
set: env.wrap(() => {}),
configurable: true,
});
try {
return env.apply(listener, this, [e]);
} finally {
env.def(e, "preventDefault", {
value: realPreventDefault,
writable: true,
configurable: true,
});
if (returnValueDesc) env.def(e, "returnValue", returnValueDesc);
else delete e.returnValue;
}
});
listenerMap.set(listener, wrapped);
}
return env.apply(nativeAddListener, this, [type, wrapped, options]);
}, "addEventListener");
env.EventTargetProto.removeEventListener = env.func(
function removeEventListener(type, listener, options) {
if (
blockedEvents.has(type) &&
typeof listener === "function" &&
isWindow(this)
) {
const wrapped = listenerMap.get(listener);
if (wrapped)
return env.apply(nativeRemoveListener, this, [
type,
wrapped,
options,
]);
}
return env.apply(nativeRemoveListener, this, [type, listener, options]);
},
"removeEventListener",
);
const nativeQuerySelector = env.DocumentProto.querySelector;
env.DocumentProto.querySelector = env.func(function querySelector(
selector,
) {
if (selector === "[disable-devtool-auto]") {
detectorPresent = true;
return null;
}
return env.apply(nativeQuerySelector, this, [selector]);
}, "querySelector");
}
function hasXray() {
try {
const page = window.wrappedJSObject;
if (!page || page === window) return false;
if (typeof exportFunction !== "function") return false;
const fn = exportFunction(() => true, page);
page.__xtest = fn;
const success = page.__xtest() === true;
delete page.__xtest;
return success;
} catch {
return false;
}
}
if (hasXray()) xraySetup();
else pageSetup();
function xraySetup() {
const page = window.wrappedJSObject;
const exportFn = (fn) => exportFunction(fn, page);
const pageObj = () => new page.Object();
const pageArr = () => new page.Array();
coreSetup({
blockedEvents: BLOCKED_EVENTS,
win: page,
doc: page.document,
console: page.console,
WindowProto: page.Window.prototype,
DocumentProto: page.Document.prototype,
EventTargetProto: page.EventTarget.prototype,
ObjectProto: page.Object.prototype,
apply: (fn, ctx, args) => Reflect.apply(fn, ctx, args),
clearTimeout: (id) => page.clearTimeout(id),
setTimeout: (fn, delay) => page.setTimeout(exportFn(fn), delay),
isDate: (val) => val instanceof page.Date,
isRegExp: (val) => val instanceof page.RegExp,
isElement: (val) => val instanceof page.Element,
isArray: (val) => page.Array.isArray(val),
keys: (obj) => page.Object.keys(obj),
desc: (obj, prop) => page.Object.getOwnPropertyDescriptor(obj, prop),
def: (obj, prop, descriptor) =>
page.Object.defineProperty(obj, prop, descriptor),
getProto: (obj) => page.Object.getPrototypeOf(obj),
newArray: pageArr,
newObject: pageObj,
newSet: (items) => new Set(items),
newWeakMap: () => new WeakMap(),
wrap: exportFn,
func: exportFn,
getter: (fn) => {
const descriptor = pageObj();
descriptor.get = exportFn(fn);
descriptor.configurable = true;
descriptor.enumerable = true;
return descriptor;
},
proxy: (target, handler) => new page.Proxy(target, handler),
reflectApply: (target, ctx, args) => Reflect.apply(target, ctx, args),
reflectConstruct: (target, args, newTarget) =>
Reflect.construct(target, args, newTarget),
stop: (e) => e.stopImmediatePropagation(),
on: (target, evt, fn, capture) =>
target.addEventListener(evt, fn, capture),
patchDescriptor: (prop, getValue) => {
const nativeDesc = (obj, key) =>
page.Object.getOwnPropertyDescriptor(obj, key);
const patched = exportFn(function getOwnPropertyDescriptor(obj, key) {
const descriptor = nativeDesc(obj, key);
if (
descriptor &&
(descriptor.get || descriptor.set) &&
key === prop
) {
return {
value: getValue(),
writable: true,
configurable: true,
enumerable: true,
};
}
return descriptor;
});
page.Object.getOwnPropertyDescriptor = patched;
page.Reflect.getOwnPropertyDescriptor = patched;
},
});
}
function pageSetup() {
const nativeStrings = new Map();
const origToString = Function.prototype.toString;
const nativeTemplate = origToString.call(document.getElementById);
const toStringHandler = {
apply(target, thisArg, args) {
return nativeStrings.has(thisArg)
? nativeStrings.get(thisArg)
: Reflect.apply(target, thisArg, args);
},
};
const toStringProxy = new Proxy(origToString, toStringHandler);
Function.prototype.toString = toStringProxy;
nativeStrings.set(
toStringProxy,
nativeTemplate.replace("getElementById", "toString"),
);
const markNative = (fn, name) => {
nativeStrings.set(
fn,
nativeTemplate.replace("getElementById", name || ""),
);
return fn;
};
coreSetup({
blockedEvents: BLOCKED_EVENTS,
win: window,
doc: document,
console: console,
WindowProto: Window.prototype,
DocumentProto: Document.prototype,
EventTargetProto: EventTarget.prototype,
ObjectProto: Object.prototype,
apply: Reflect.apply,
clearTimeout: clearTimeout.bind(window),
setTimeout: setTimeout.bind(window),
isDate: (val) => val instanceof Date,
isRegExp: (val) => val instanceof RegExp,
isElement: (val) => val instanceof Element,
isArray: Array.isArray,
keys: Object.keys,
desc: Object.getOwnPropertyDescriptor,
def: Object.defineProperty,
getProto: Object.getPrototypeOf,
newArray: () => [],
newObject: () => ({}),
newSet: (items) => new Set(items),
newWeakMap: () => new WeakMap(),
wrap: (fn) => fn,
func: (fn, name) => markNative(fn, name),
getter: (fn, name) => ({
get: markNative(fn, name),
configurable: true,
enumerable: true,
}),
proxy: (target, handler, name) => {
const proxy = new Proxy(target, handler);
if (name) markNative(proxy, name);
return proxy;
},
reflectApply: Reflect.apply,
reflectConstruct: Reflect.construct,
stop: (e) => e.stopImmediatePropagation(),
on: (target, evt, fn, capture) =>
EventTarget.prototype.addEventListener.call(target, evt, fn, capture),
patchDescriptor: (prop, getValue) => {
const nativeDesc = Object.getOwnPropertyDescriptor;
const patched = markNative(function getOwnPropertyDescriptor(obj, key) {
const descriptor = nativeDesc(obj, key);
if (
descriptor &&
(descriptor.get || descriptor.set) &&
key === prop
) {
return {
value: getValue(),
writable: true,
configurable: true,
enumerable: true,
};
}
return descriptor;
}, "getOwnPropertyDescriptor");
Object.getOwnPropertyDescriptor = patched;
Reflect.getOwnPropertyDescriptor = patched;
},
});
}
})();