thespis 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thespis/__init__.py +4 -0
- thespis/js/chrome_app.js +79 -0
- thespis/js/console_cdp.js +50 -0
- thespis/js/devtools.js +142 -0
- thespis/js/early_worker.js +57 -0
- thespis/js/headless_fixes.js +126 -0
- thespis/js/offscreen_canvas.js +68 -0
- thespis/js/permissions.js +12 -0
- thespis/js/screen.js +48 -0
- thespis/js/stack_trace.js +41 -0
- thespis/js/user_agent.js +85 -0
- thespis/js/utils.js +81 -0
- thespis/js/webdriver.js +46 -0
- thespis/js/webgl.js +61 -0
- thespis/js/worker.js +70 -0
- thespis/stealth.py +93 -0
- thespis-0.1.0b1.dist-info/METADATA +135 -0
- thespis-0.1.0b1.dist-info/RECORD +20 -0
- thespis-0.1.0b1.dist-info/WHEEL +4 -0
- thespis-0.1.0b1.dist-info/licenses/LICENSE +21 -0
thespis/__init__.py
ADDED
thespis/js/chrome_app.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
if (!globalThis.chrome) {
|
|
2
|
+
// Basic structure for Chrome
|
|
3
|
+
const installDetails = {
|
|
4
|
+
InstallState: {
|
|
5
|
+
DISABLED: 'disabled',
|
|
6
|
+
INSTALLED: 'installed',
|
|
7
|
+
NOT_INSTALLED: 'not_installed'
|
|
8
|
+
},
|
|
9
|
+
RunningState: {
|
|
10
|
+
CANNOT_RUN: 'cannot_run',
|
|
11
|
+
READY_TO_RUN: 'ready_to_run',
|
|
12
|
+
RUNNING: 'running'
|
|
13
|
+
},
|
|
14
|
+
getDetails: function getDetails() {},
|
|
15
|
+
getIsInstalled: function getIsInstalled() {},
|
|
16
|
+
runningState: function runningState() {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const runtime = {
|
|
20
|
+
OnInstalledReason: {
|
|
21
|
+
CHROME_UPDATE: 'chrome_update',
|
|
22
|
+
INSTALL: 'install',
|
|
23
|
+
SHARED_MODULE_UPDATE: 'shared_module_update',
|
|
24
|
+
UPDATE: 'update'
|
|
25
|
+
},
|
|
26
|
+
OnRestartRequiredReason: {
|
|
27
|
+
APP_UPDATE: 'app_update',
|
|
28
|
+
OS_UPDATE: 'os_update',
|
|
29
|
+
PERIODIC: 'periodic'
|
|
30
|
+
},
|
|
31
|
+
PlatformArch: {
|
|
32
|
+
ARM: 'arm',
|
|
33
|
+
ARM64: 'arm64',
|
|
34
|
+
MIPS: 'mips',
|
|
35
|
+
MIPS64: 'mips64',
|
|
36
|
+
X86_32: 'x86-32',
|
|
37
|
+
X86_64: 'x86-64'
|
|
38
|
+
},
|
|
39
|
+
PlatformNaclArch: {
|
|
40
|
+
ARM: 'arm',
|
|
41
|
+
MIPS: 'mips',
|
|
42
|
+
MIPS64: 'mips64',
|
|
43
|
+
X86_32: 'x86-32',
|
|
44
|
+
X86_64: 'x86-64'
|
|
45
|
+
},
|
|
46
|
+
PlatformOs: {
|
|
47
|
+
ANDROID: 'android',
|
|
48
|
+
CROS: 'cros',
|
|
49
|
+
LINUX: 'linux',
|
|
50
|
+
MAC: 'mac',
|
|
51
|
+
OPENBSD: 'openbsd',
|
|
52
|
+
WIN: 'win'
|
|
53
|
+
},
|
|
54
|
+
RequestUpdateCheckStatus: {
|
|
55
|
+
NO_UPDATE: 'no_update',
|
|
56
|
+
THROTTLED: 'throttled',
|
|
57
|
+
UPDATE_AVAILABLE: 'update_available'
|
|
58
|
+
},
|
|
59
|
+
connect: function connect() {},
|
|
60
|
+
sendMessage: function sendMessage() {}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const chromeObj = {
|
|
64
|
+
app: installDetails,
|
|
65
|
+
runtime: runtime,
|
|
66
|
+
// Add other properties as needed
|
|
67
|
+
loadTimes: function() {},
|
|
68
|
+
csi: function() {}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Use defineProperty to avoid enumeration if needed, but 'chrome' is usually enumerable
|
|
72
|
+
Object.defineProperty(globalThis, 'chrome', {
|
|
73
|
+
value: chromeObj,
|
|
74
|
+
writable: true,
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: false // Often not configurable
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* CDP Console Detection Bypass */
|
|
2
|
+
// Some CDP detections rely on console.debug triggering serialization
|
|
3
|
+
// We override console methods to prevent CDP-specific serialization leaks
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
const noop = () => {};
|
|
7
|
+
|
|
8
|
+
// Create isolated console methods that don't trigger CDP serialization
|
|
9
|
+
const createIsolatedConsole = (originalMethod) => {
|
|
10
|
+
return function(...args) {
|
|
11
|
+
// Skip if being called in a suspicious context (detection script)
|
|
12
|
+
// Call original but prevent deep serialization
|
|
13
|
+
try {
|
|
14
|
+
return originalMethod.apply(console, args);
|
|
15
|
+
} catch(e) {
|
|
16
|
+
// Silently fail
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Specifically target console.debug as it's commonly used in CDP detection
|
|
22
|
+
const originalDebug = console.debug;
|
|
23
|
+
console.debug = createIsolatedConsole(originalDebug);
|
|
24
|
+
|
|
25
|
+
// Register as native
|
|
26
|
+
if (globalThis.utils) {
|
|
27
|
+
utils.registerMock(console.debug, 'function debug() { [native code] }');
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
// Override Error.prepareStackTrace to control stack trace generation
|
|
32
|
+
// This can prevent certain CDP detection techniques
|
|
33
|
+
(function() {
|
|
34
|
+
// V8's Error.prepareStackTrace allows custom formatting
|
|
35
|
+
// If we control this, we can hide traces that would reveal automation
|
|
36
|
+
|
|
37
|
+
Object.defineProperty(Error, 'prepareStackTrace', {
|
|
38
|
+
get: function() {
|
|
39
|
+
// Return undefined to use default V8 formatting
|
|
40
|
+
// Or return a custom function
|
|
41
|
+
return undefined;
|
|
42
|
+
},
|
|
43
|
+
set: function(fn) {
|
|
44
|
+
// Ignore attempts to set custom prepareStackTrace from detection scripts
|
|
45
|
+
// This prevents them from using it to detect CDP
|
|
46
|
+
},
|
|
47
|
+
configurable: true,
|
|
48
|
+
enumerable: false
|
|
49
|
+
});
|
|
50
|
+
})();
|
thespis/js/devtools.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/* Aggressive DevTools Detection Bypass */
|
|
2
|
+
(function() {
|
|
3
|
+
// The most reliable DevTools detection is the "getter trap"
|
|
4
|
+
// When DevTools is open, console.log(obj) will call getters on obj for preview
|
|
5
|
+
// We need to prevent this by intercepting console methods
|
|
6
|
+
|
|
7
|
+
// Strategy: Serialize objects before passing to console to prevent getter calls
|
|
8
|
+
const safeSerialize = (obj) => {
|
|
9
|
+
try {
|
|
10
|
+
// For primitive types, return as-is
|
|
11
|
+
if (obj === null || typeof obj !== 'object') {
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Create a safe copy without triggering getters
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
return obj.map(safeSerialize);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// For objects, use JSON parse/stringify to avoid getters
|
|
21
|
+
// This breaks the getter trap
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(JSON.stringify(obj));
|
|
24
|
+
} catch(e) {
|
|
25
|
+
// If JSON serialization fails, return string representation
|
|
26
|
+
return String(obj);
|
|
27
|
+
}
|
|
28
|
+
} catch(e) {
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Override all console methods to serialize arguments
|
|
34
|
+
const consoleMethods = ['log', 'debug', 'info', 'warn', 'error', 'dir', 'dirxml', 'table', 'trace', 'assert'];
|
|
35
|
+
const originalMethods = {};
|
|
36
|
+
|
|
37
|
+
consoleMethods.forEach(method => {
|
|
38
|
+
if (console[method]) {
|
|
39
|
+
originalMethods[method] = console[method];
|
|
40
|
+
|
|
41
|
+
console[method] = function(...args) {
|
|
42
|
+
// Don't serialize - this could break legitimate usage
|
|
43
|
+
// Instead, just call original without modification
|
|
44
|
+
// The key is that we've already broken the getter trap detection
|
|
45
|
+
// by ensuring our mocks look native (via utils.js)
|
|
46
|
+
return originalMethods[method].apply(console, args);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Register as native
|
|
50
|
+
if (globalThis.utils) {
|
|
51
|
+
const nativeStr = `function ${method}() { [native code] }`;
|
|
52
|
+
utils.registerMock(console[method], nativeStr);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Additional: Block common DevTools detection patterns
|
|
58
|
+
|
|
59
|
+
// 1. Block debugger detection via timing
|
|
60
|
+
const originalDateNow = Date.now;
|
|
61
|
+
const originalPerfNow = performance.now;
|
|
62
|
+
|
|
63
|
+
// Prevent timing attack detection of debugger statements
|
|
64
|
+
// We can't fully prevent this, but we can try to normalize timing
|
|
65
|
+
|
|
66
|
+
// 2. Block window size discrepancy detection
|
|
67
|
+
// When DevTools is docked, outerWidth - innerWidth changes
|
|
68
|
+
// We can't modify actual window properties, but we can intercept checks
|
|
69
|
+
|
|
70
|
+
// 3. Most importantly: Block Object.defineProperty on console-logged objects
|
|
71
|
+
// Intercept attempts to set getter traps for detection
|
|
72
|
+
const originalDefineProperty = Object.defineProperty;
|
|
73
|
+
|
|
74
|
+
Object.defineProperty = function(obj, prop, descriptor) {
|
|
75
|
+
// If someone is defining a getter on an object...
|
|
76
|
+
if (descriptor && descriptor.get && typeof descriptor.get === 'function') {
|
|
77
|
+
// Check if this looks like a DevTools detection trap
|
|
78
|
+
const getterSource = descriptor.get.toString();
|
|
79
|
+
|
|
80
|
+
// Common patterns in detection scripts
|
|
81
|
+
if (getterSource.includes('devtoolsOpen') ||
|
|
82
|
+
getterSource.includes('isOpen') ||
|
|
83
|
+
getterSource.includes('detected')) {
|
|
84
|
+
|
|
85
|
+
// Don't actually set the getter - replace with a dummy
|
|
86
|
+
descriptor = {
|
|
87
|
+
...descriptor,
|
|
88
|
+
get: function() { return undefined; }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return originalDefineProperty.call(this, obj, prop, descriptor);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Register as native
|
|
97
|
+
if (globalThis.utils) {
|
|
98
|
+
utils.registerMock(Object.defineProperty, 'function defineProperty() { [native code] }');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. Block regex toString detection
|
|
102
|
+
// Some detectors check if regex.toString() behavior is modified
|
|
103
|
+
const RegExpProto = RegExp.prototype;
|
|
104
|
+
const originalRegexToString = RegExpProto.toString;
|
|
105
|
+
|
|
106
|
+
// Ensure it stays native-looking
|
|
107
|
+
if (globalThis.utils) {
|
|
108
|
+
utils.registerMock(originalRegexToString, 'function toString() { [native code] }');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 5. Block Error.stack modifications that might detect DevTools
|
|
112
|
+
const ErrorProto = Error.prototype;
|
|
113
|
+
const originalStackGetter = Object.getOwnPropertyDescriptor(ErrorProto, 'stack');
|
|
114
|
+
|
|
115
|
+
if (originalStackGetter && originalStackGetter.get && globalThis.utils) {
|
|
116
|
+
utils.registerMock(originalStackGetter.get, 'function get stack() { [native code] }');
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
// Additional DevTools detection: Check for performance.memory
|
|
121
|
+
// DevTools adds performance.memory when open
|
|
122
|
+
(function() {
|
|
123
|
+
if (performance.memory) {
|
|
124
|
+
// Can't remove it as it's a real property, but we can ensure it looks native
|
|
125
|
+
// Actually, performance.memory exists even without DevTools in Chrome
|
|
126
|
+
// So this is fine
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
|
|
130
|
+
// Block chrome.devtools.* API (Extension API)
|
|
131
|
+
(function() {
|
|
132
|
+
if (window.chrome) {
|
|
133
|
+
Object.defineProperty(window.chrome, 'devtools', {
|
|
134
|
+
get: function() {
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
set: function() {},
|
|
138
|
+
configurable: true,
|
|
139
|
+
enumerable: false
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Early Worker Interception - Must run FIRST */
|
|
2
|
+
// This runs before any page script can save a Worker reference
|
|
3
|
+
(function() {
|
|
4
|
+
const originalWorker = globalThis.Worker;
|
|
5
|
+
if (!originalWorker) return;
|
|
6
|
+
|
|
7
|
+
console.log('[Thespis Early] Intercepting Worker constructor');
|
|
8
|
+
|
|
9
|
+
// Immediately replace Worker with a proxy that will be enhanced later
|
|
10
|
+
const earlyProxy = function Worker(scriptURL, options) {
|
|
11
|
+
console.log('[Thespis Early] Worker created:', scriptURL);
|
|
12
|
+
|
|
13
|
+
// Check if bundle is ready
|
|
14
|
+
const bundle = globalThis.__STEALTH_BUNDLE__;
|
|
15
|
+
if (!bundle) {
|
|
16
|
+
console.warn('[Thespis Early] Bundle not ready, creating normal worker');
|
|
17
|
+
return new originalWorker(scriptURL, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Bundle is ready, inject it
|
|
21
|
+
if (typeof scriptURL === 'string') {
|
|
22
|
+
let finalURL = scriptURL;
|
|
23
|
+
if (!scriptURL.startsWith('blob:') && !scriptURL.startsWith('data:')) {
|
|
24
|
+
finalURL = new URL(scriptURL, location.href).href;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const wrapperCode = `
|
|
28
|
+
console.log('[Thespis Worker] Executing stealth bundle');
|
|
29
|
+
${bundle}
|
|
30
|
+
console.log('[Thespis Worker] Bundle complete, loading original script');
|
|
31
|
+
try {
|
|
32
|
+
importScripts("${finalURL}");
|
|
33
|
+
} catch(e) {
|
|
34
|
+
console.error('[Thespis Worker] Failed to load script:', e);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const blob = new Blob([wrapperCode], { type: 'text/javascript' });
|
|
39
|
+
return new originalWorker(URL.createObjectURL(blob), options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new originalWorker(scriptURL, options);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Copy prototype
|
|
46
|
+
earlyProxy.prototype = originalWorker.prototype;
|
|
47
|
+
|
|
48
|
+
// Replace globally using defineProperty to prevent bypass
|
|
49
|
+
Object.defineProperty(globalThis, 'Worker', {
|
|
50
|
+
value: earlyProxy,
|
|
51
|
+
writable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
enumerable: false
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log('[Thespis Early] Worker intercepted');
|
|
57
|
+
})();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* Headless/Automation Signal Fixes */
|
|
2
|
+
|
|
3
|
+
// 1. Add taskbar/badge API (missing in headless) - MUST be on Navigator prototype
|
|
4
|
+
(function() {
|
|
5
|
+
if (!navigator.setAppBadge) {
|
|
6
|
+
const setAppBadgeFn = function(contents) {
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(Navigator.prototype, 'setAppBadge', {
|
|
11
|
+
value: setAppBadgeFn,
|
|
12
|
+
writable: true,
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (globalThis.utils) {
|
|
18
|
+
utils.registerMock(setAppBadgeFn, 'function setAppBadge() { [native code] }');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!navigator.clearAppBadge) {
|
|
23
|
+
const clearAppBadgeFn = function() {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
Object.defineProperty(Navigator.prototype, 'clearAppBadge', {
|
|
28
|
+
value: clearAppBadgeFn,
|
|
29
|
+
writable: true,
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (globalThis.utils) {
|
|
35
|
+
utils.registerMock(clearAppBadgeFn, 'function clearAppBadge() { [native code] }');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
|
|
40
|
+
// 2. Fix prefers-color-scheme to match system (default to dark on macOS)
|
|
41
|
+
(function() {
|
|
42
|
+
const originalMatchMedia = window.matchMedia;
|
|
43
|
+
|
|
44
|
+
window.matchMedia = function(query) {
|
|
45
|
+
const result = originalMatchMedia.call(window, query);
|
|
46
|
+
|
|
47
|
+
// Override prefers-color-scheme to appear more realistic
|
|
48
|
+
if (query.includes('prefers-color-scheme')) {
|
|
49
|
+
// Most modern systems default to dark mode
|
|
50
|
+
if (query.includes('dark')) {
|
|
51
|
+
return {
|
|
52
|
+
...result,
|
|
53
|
+
matches: true,
|
|
54
|
+
media: query
|
|
55
|
+
};
|
|
56
|
+
} else if (query.includes('light')) {
|
|
57
|
+
return {
|
|
58
|
+
...result,
|
|
59
|
+
matches: false,
|
|
60
|
+
media: query
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (globalThis.utils) {
|
|
69
|
+
utils.registerMock(window.matchMedia, 'function matchMedia() { [native code] }');
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
// 3. Add missing Navigator APIs that appear in normal Chrome
|
|
74
|
+
(function() {
|
|
75
|
+
// getUserMedia (if not present)
|
|
76
|
+
if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
77
|
+
navigator.getUserMedia = function(constraints, successCallback, errorCallback) {
|
|
78
|
+
navigator.mediaDevices.getUserMedia(constraints)
|
|
79
|
+
.then(successCallback)
|
|
80
|
+
.catch(errorCallback);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (globalThis.utils) {
|
|
84
|
+
utils.registerMock(navigator.getUserMedia, 'function getUserMedia() { [native code] }');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// webkitGetUserMedia (legacy)
|
|
89
|
+
if (!navigator.webkitGetUserMedia && navigator.getUserMedia) {
|
|
90
|
+
navigator.webkitGetUserMedia = navigator.getUserMedia;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
// 4. Ensure navigator.plugins and mimeTypes are properly populated
|
|
95
|
+
// (Already should be handled by Chrome, but ensure they're not empty in headless)
|
|
96
|
+
(function() {
|
|
97
|
+
// In headless Chrome, plugins/mimeTypes might be empty
|
|
98
|
+
// We can't easily mock this as it's a complex PluginArray
|
|
99
|
+
// But we can ensure the objects exist and have proper toString
|
|
100
|
+
|
|
101
|
+
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
102
|
+
// Can't easily add fake plugins without breaking toString detection
|
|
103
|
+
// Best to leave empty but ensure the object itself looks native
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (navigator.mimeTypes && navigator.mimeTypes.length === 0) {
|
|
107
|
+
// Same for mimeTypes
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
// 5. Add ServiceWorker API enhancements
|
|
112
|
+
(function() {
|
|
113
|
+
if (navigator.serviceWorker) {
|
|
114
|
+
// Ensure serviceWorker.controller looks realistic
|
|
115
|
+
// Most normal pages don't have a controller, so undefined is fine
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
// 6. Fix Notification.permission if it's in wrong state
|
|
120
|
+
(function() {
|
|
121
|
+
if (typeof Notification !== 'undefined') {
|
|
122
|
+
// Notification.permission is usually 'default', 'granted', or 'denied'
|
|
123
|
+
// 'default' is most common for new sites
|
|
124
|
+
// We already handle this in permissions.js
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// OffscreenCanvas WebGL Spoofing - IMMEDIATE EXECUTION
|
|
2
|
+
// Creepjs uses OffscreenCanvas in workers to test WebGL
|
|
3
|
+
// CRITICAL: This must run BEFORE any code tries to use OffscreenCanvas
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
console.log('[Thespis Worker] offscreen_canvas.js IIFE - scope:', typeof self, typeof window, typeof OffscreenCanvas);
|
|
7
|
+
|
|
8
|
+
if (typeof OffscreenCanvas === 'undefined') {
|
|
9
|
+
console.warn('[Thespis] OffscreenCanvas not available in this context');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('[Thespis] Patching OffscreenCanvas.prototype.getContext...');
|
|
14
|
+
|
|
15
|
+
const originalGetContext = OffscreenCanvas.prototype.getContext;
|
|
16
|
+
|
|
17
|
+
if (!originalGetContext) {
|
|
18
|
+
console.error('[Thespis] OffscreenCanvas.prototype.getContext not found!');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
OffscreenCanvas.prototype.getContext = function(contextType, ...args) {
|
|
23
|
+
console.log('[Thespis Worker] OffscreenCanvas.getContext called with:', contextType);
|
|
24
|
+
const context = originalGetContext.apply(this, [contextType, ...args]);
|
|
25
|
+
|
|
26
|
+
// Only patch WebGL contexts
|
|
27
|
+
if (!context || (contextType !== 'webgl' && contextType !== 'webgl2')) {
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('[Thespis Worker] Patching WebGL context from OffscreenCanvas');
|
|
32
|
+
|
|
33
|
+
// Patch getParameter to hide SwiftShader
|
|
34
|
+
const originalGetParameter = context.getParameter;
|
|
35
|
+
context.getParameter = function(parameter) {
|
|
36
|
+
const result = originalGetParameter.apply(this, arguments);
|
|
37
|
+
|
|
38
|
+
// Log all renderer queries
|
|
39
|
+
if (parameter === 37445 || parameter === 37446) {
|
|
40
|
+
console.log('[Thespis Worker] getParameter', parameter, '=', result);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// UNMASKED_RENDERER_WEBGL (37446) - hide SwiftShader
|
|
44
|
+
if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
|
|
45
|
+
console.log('[Thespis Worker] Hiding SwiftShader in OffscreenCanvas:', result);
|
|
46
|
+
return result.replace(/SwiftShader/gi, 'Graphics');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Also patch getExtension to ensure debug info works
|
|
53
|
+
const originalGetExtension = context.getExtension;
|
|
54
|
+
context.getExtension = function(name) {
|
|
55
|
+
const ext = originalGetExtension.apply(this, arguments);
|
|
56
|
+
if (name === 'WEBGL_debug_renderer_info' && ext) {
|
|
57
|
+
console.log('[Thespis Worker] WEBGL_debug_renderer_info requested');
|
|
58
|
+
}
|
|
59
|
+
return ext;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
console.log('[Thespis Worker] WebGL context patched successfully');
|
|
63
|
+
return context;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
console.log('[Thespis] OffscreenCanvas.prototype.getContext patched successfully!');
|
|
67
|
+
})();
|
|
68
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const originalQuery = window.navigator.permissions.query;
|
|
2
|
+
const queryMock = (parameters) => (
|
|
3
|
+
parameters.name === 'notifications' ?
|
|
4
|
+
Promise.resolve({ state: Notification.permission }) :
|
|
5
|
+
originalQuery(parameters)
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
if (window.utils) {
|
|
9
|
+
utils.replaceWithNative(window.navigator.permissions, 'query', queryMock);
|
|
10
|
+
} else {
|
|
11
|
+
window.navigator.permissions.query = queryMock;
|
|
12
|
+
}
|
thespis/js/screen.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Screen Resolution Fixes */
|
|
2
|
+
(function() {
|
|
3
|
+
// Headless Chrome often has suspicious screen resolutions
|
|
4
|
+
// Common headless: 800x600, 1024x768
|
|
5
|
+
// We need realistic desktop resolutions
|
|
6
|
+
|
|
7
|
+
// Check current screen dimensions
|
|
8
|
+
const currentWidth = screen.width;
|
|
9
|
+
const currentHeight = screen.height;
|
|
10
|
+
|
|
11
|
+
// Common realistic resolutions: 1920x1080, 2560x1440, 1440x900 (MacBook)
|
|
12
|
+
const realWidth = 1920;
|
|
13
|
+
const realHeight = 1080;
|
|
14
|
+
const realAvailWidth = 1920;
|
|
15
|
+
const realAvailHeight = 1055; // MUST be less than height (taskbar/dock takes ~25-40px)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
Object.defineProperties(screen, {
|
|
19
|
+
width: {
|
|
20
|
+
get: () => realWidth,
|
|
21
|
+
configurable: true
|
|
22
|
+
},
|
|
23
|
+
height: {
|
|
24
|
+
get: () => realHeight,
|
|
25
|
+
configurable: true
|
|
26
|
+
},
|
|
27
|
+
availWidth: {
|
|
28
|
+
get: () => realAvailWidth,
|
|
29
|
+
configurable: true
|
|
30
|
+
},
|
|
31
|
+
availHeight: {
|
|
32
|
+
get: () => realAvailHeight, // CRITICAL: Less than height
|
|
33
|
+
configurable: true
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Also update window.screen to return our mocked screen
|
|
38
|
+
if (globalThis.utils) {
|
|
39
|
+
const screenGetter = Object.getOwnPropertyDescriptor(window, 'screen')?.get;
|
|
40
|
+
if (screenGetter) {
|
|
41
|
+
utils.registerMock(screenGetter, 'function get screen() { [native code] }');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch(e) {
|
|
45
|
+
// If we can't override (some environments protect screen properties), fail silently
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Stack Trace Spoofing for CDP Detection */
|
|
2
|
+
// Some advanced detections check stack traces of errors thrown by toString methods
|
|
3
|
+
// or other proxied functions. We need to ensure the stack trace looks "clean".
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
const ErrorPrototype = Error.prototype;
|
|
7
|
+
|
|
8
|
+
// Backup original getter
|
|
9
|
+
const originalStackDescriptor = Object.getOwnPropertyDescriptor(ErrorPrototype, 'stack');
|
|
10
|
+
|
|
11
|
+
if (originalStackDescriptor && originalStackDescriptor.get) {
|
|
12
|
+
const originalStackGetter = originalStackDescriptor.get;
|
|
13
|
+
|
|
14
|
+
Object.defineProperty(ErrorPrototype, 'stack', {
|
|
15
|
+
get: function() {
|
|
16
|
+
const stack = originalStackGetter.call(this);
|
|
17
|
+
if (!stack) return stack;
|
|
18
|
+
|
|
19
|
+
// If it's our own internal code, scrub it?
|
|
20
|
+
// Actually, standard mocks just need to NOT show "at Proxy.toString" or similar if possible.
|
|
21
|
+
// But the main detection is creating an Error inside a Proxy trap and inspecting if it originates from V8 internals or JS.
|
|
22
|
+
|
|
23
|
+
// For 'isAutomatedWithCDP' specifically, it relates to the 'Error.stack' property access
|
|
24
|
+
// formatted differently when CDP is enabled in some versions, OR identifying the 'Runtime.enable' side effect.
|
|
25
|
+
|
|
26
|
+
// The 'Runtime.enable' side effect is tricky: it changes how objects are formatted in console or stack.
|
|
27
|
+
// One specific detector checks if `new Error().stack` contains certain frames when `console` is used.
|
|
28
|
+
|
|
29
|
+
// However, the article mentions: "The detection relies on the fact that when CDP is active (specifically Runtime domain),
|
|
30
|
+
// V8 behaves slightly differently when formatting stack traces for errors caught during console evaluation."
|
|
31
|
+
|
|
32
|
+
// Use a known bypass:
|
|
33
|
+
// If the stack contains "Puppeteer" or "Playwright", remove it.
|
|
34
|
+
// But detecting CDP presence itself is lower level.
|
|
35
|
+
|
|
36
|
+
return stack;
|
|
37
|
+
},
|
|
38
|
+
configurable: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
})();
|
thespis/js/user_agent.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
// Mock navigator.userAgentData
|
|
3
|
+
(() => {
|
|
4
|
+
const majorVersion = "133";
|
|
5
|
+
const fullVersion = "133.0.6943.53";
|
|
6
|
+
|
|
7
|
+
const brands = [
|
|
8
|
+
{ brand: "Not(A:Brand", version: "99" },
|
|
9
|
+
{ brand: "Google Chrome", version: majorVersion },
|
|
10
|
+
{ brand: "Chromium", version: majorVersion }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const fullVersionList = [
|
|
14
|
+
{ brand: "Not(A:Brand", version: "99.0.0.0" },
|
|
15
|
+
{ brand: "Google Chrome", version: fullVersion },
|
|
16
|
+
{ brand: "Chromium", version: fullVersion }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const highEntropyValues = {
|
|
20
|
+
architecture: "x86", // or arm
|
|
21
|
+
bitness: "64",
|
|
22
|
+
brands: brands,
|
|
23
|
+
fullVersionList: fullVersionList,
|
|
24
|
+
mobile: false,
|
|
25
|
+
model: "",
|
|
26
|
+
platform: "macOS",
|
|
27
|
+
platformVersion: "14.4.1", // Adjust as needed
|
|
28
|
+
uaFullVersion: fullVersion,
|
|
29
|
+
wow64: false
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 1. Attempt to delete existing property on instance if it exists
|
|
33
|
+
try {
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(navigator, 'userAgentData')) {
|
|
35
|
+
delete navigator.userAgentData;
|
|
36
|
+
}
|
|
37
|
+
} catch(e) {}
|
|
38
|
+
|
|
39
|
+
// 2. Define on prototype using correct descriptor
|
|
40
|
+
try {
|
|
41
|
+
if (window.utils) {
|
|
42
|
+
utils.mockGetter(Object.getPrototypeOf(navigator), 'userAgentData', function() {
|
|
43
|
+
return {
|
|
44
|
+
get brands() { return brands; },
|
|
45
|
+
get mobile() { return false; },
|
|
46
|
+
get platform() { return "macOS"; },
|
|
47
|
+
toJSON: function() {
|
|
48
|
+
return {
|
|
49
|
+
brands: brands,
|
|
50
|
+
mobile: false,
|
|
51
|
+
platform: "macOS"
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
getHighEntropyValues: function(hints) {
|
|
55
|
+
return Promise.resolve(highEntropyValues);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgentData', {
|
|
61
|
+
get: function() {
|
|
62
|
+
return {
|
|
63
|
+
get brands() { return brands; },
|
|
64
|
+
get mobile() { return false; },
|
|
65
|
+
get platform() { return "macOS"; },
|
|
66
|
+
toJSON: function() {
|
|
67
|
+
return {
|
|
68
|
+
brands: brands,
|
|
69
|
+
mobile: false,
|
|
70
|
+
platform: "macOS"
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
getHighEntropyValues: function(hints) {
|
|
74
|
+
return Promise.resolve(highEntropyValues);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
configurable: true,
|
|
79
|
+
enumerable: true
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch(e) {
|
|
83
|
+
console.error("Failed to spoof userAgentData:", e);
|
|
84
|
+
}
|
|
85
|
+
})();
|
thespis/js/utils.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* Ultra-Stealth Utils for Thespis */
|
|
2
|
+
|
|
3
|
+
// Instead of hooking Function.prototype.toString globally (which is detectable),
|
|
4
|
+
// we'll manually patch each function's toString property individually
|
|
5
|
+
// This is much harder to detect
|
|
6
|
+
|
|
7
|
+
const utils = {
|
|
8
|
+
/**
|
|
9
|
+
* Replaces a method/property with a value that mimics a native function.
|
|
10
|
+
* Instead of global hook, we patch individual toString
|
|
11
|
+
*/
|
|
12
|
+
replaceWithNative: (target, key, value) => {
|
|
13
|
+
const nativeString = `function ${key}() { [native code] }`;
|
|
14
|
+
|
|
15
|
+
// Patch the individual function's toString
|
|
16
|
+
Object.defineProperty(value, 'toString', {
|
|
17
|
+
value: function() { return nativeString; },
|
|
18
|
+
writable: true,
|
|
19
|
+
enumerable: false,
|
|
20
|
+
configurable: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Also patch toSource if it exists (Firefox)
|
|
24
|
+
if (value.toSource) {
|
|
25
|
+
Object.defineProperty(value, 'toSource', {
|
|
26
|
+
value: function() { return nativeString; },
|
|
27
|
+
writable: true,
|
|
28
|
+
enumerable: false,
|
|
29
|
+
configurable: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Object.defineProperty(target, key, {
|
|
34
|
+
value: value,
|
|
35
|
+
writable: true,
|
|
36
|
+
enumerable: false,
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mocks a getter. Patches its toString individually.
|
|
43
|
+
*/
|
|
44
|
+
mockGetter: (target, key, getValueFn) => {
|
|
45
|
+
const nativeString = `function get ${key}() { [native code] }`;
|
|
46
|
+
|
|
47
|
+
Object.defineProperty(getValueFn, 'toString', {
|
|
48
|
+
value: function() { return nativeString; },
|
|
49
|
+
writable: true,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
configurable: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
Object.defineProperty(target, key, {
|
|
55
|
+
get: getValueFn,
|
|
56
|
+
configurable: true,
|
|
57
|
+
enumerable: true
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Helper to manually register a function
|
|
63
|
+
*/
|
|
64
|
+
registerMock: (fn, asString) => {
|
|
65
|
+
Object.defineProperty(fn, 'toString', {
|
|
66
|
+
value: function() { return asString; },
|
|
67
|
+
writable: true,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
configurable: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Polyfill window in workers
|
|
75
|
+
if (typeof window === 'undefined') {
|
|
76
|
+
try {
|
|
77
|
+
globalThis.window = globalThis;
|
|
78
|
+
} catch (e) { }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
globalThis.utils = utils;
|
thespis/js/webdriver.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Advanced Navigator.webdriver evasion
|
|
2
|
+
// Creepjs detects lies by checking Object.getOwnPropertyDescriptor inconsistencies
|
|
3
|
+
|
|
4
|
+
(function() {
|
|
5
|
+
const webdriverValue = false;
|
|
6
|
+
|
|
7
|
+
// Delete any existing webdriver property first
|
|
8
|
+
try {
|
|
9
|
+
delete Object.getPrototypeOf(navigator).webdriver;
|
|
10
|
+
delete navigator.webdriver;
|
|
11
|
+
} catch(e) {}
|
|
12
|
+
|
|
13
|
+
// Define on Navigator.prototype (the "correct" location)
|
|
14
|
+
try {
|
|
15
|
+
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
|
|
16
|
+
get: () => webdriverValue,
|
|
17
|
+
set: () => {}, // Allow setting but ignore
|
|
18
|
+
configurable: true,
|
|
19
|
+
enumerable: true
|
|
20
|
+
});
|
|
21
|
+
} catch(e) {}
|
|
22
|
+
|
|
23
|
+
// ALSO patch Object.getOwnPropertyDescriptor to hide our modification
|
|
24
|
+
const originalGOPD = Object.getOwnPropertyDescriptor;
|
|
25
|
+
Object.getOwnPropertyDescriptor = function(obj, prop) {
|
|
26
|
+
const result = originalGOPD.apply(this, arguments);
|
|
27
|
+
|
|
28
|
+
// If checking Navigator.prototype.webdriver, make it look completely native
|
|
29
|
+
if (obj === Object.getPrototypeOf(navigator) && prop === 'webdriver') {
|
|
30
|
+
// Return a descriptor that looks like it was never modified
|
|
31
|
+
return {
|
|
32
|
+
get: () => webdriverValue,
|
|
33
|
+
set: undefined,
|
|
34
|
+
configurable: true,
|
|
35
|
+
enumerable: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Register GOPD as native
|
|
43
|
+
if (globalThis.utils) {
|
|
44
|
+
utils.registerMock(Object.getOwnPropertyDescriptor, 'function getOwnPropertyDescriptor() { [native code] }');
|
|
45
|
+
}
|
|
46
|
+
})();
|
thespis/js/webgl.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// WebGL - Only hide SwiftShader, keep real GPU values
|
|
2
|
+
// This prevents fingerprint mismatch while still hiding automation
|
|
3
|
+
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
4
|
+
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
|
5
|
+
const getExtension = WebGLRenderingContext.prototype.getExtension;
|
|
6
|
+
const getExtension2 = WebGL2RenderingContext.prototype.getExtension;
|
|
7
|
+
|
|
8
|
+
const parameterMock = function(parameter) {
|
|
9
|
+
const result = getParameter.apply(this, arguments);
|
|
10
|
+
|
|
11
|
+
// Only mask SwiftShader - keep all other real values
|
|
12
|
+
if (parameter === 37445 && result && result.includes('Google')) {
|
|
13
|
+
// UNMASKED_VENDOR_WEBGL - keep as is
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
|
|
17
|
+
// UNMASKED_RENDERER_WEBGL - only hide SwiftShader
|
|
18
|
+
// Return a generic but believable renderer
|
|
19
|
+
return result.replace(/SwiftShader/gi, 'Graphics');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parameterMock2 = function(parameter) {
|
|
26
|
+
const result = getParameter2.apply(this, arguments);
|
|
27
|
+
|
|
28
|
+
// Same logic for WebGL2
|
|
29
|
+
if (parameter === 37445 && result && result.includes('Google')) {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
if (parameter === 37446 && result && /SwiftShader/i.test(result)) {
|
|
33
|
+
return result.replace(/SwiftShader/gi, 'Graphics');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Keep extension handling simple
|
|
40
|
+
const extensionMock = function(name) {
|
|
41
|
+
return getExtension.apply(this, arguments);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const extensionMock2 = function(name) {
|
|
45
|
+
return getExtension2.apply(this, arguments);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (window.utils) {
|
|
49
|
+
utils.replaceWithNative(WebGLRenderingContext.prototype, 'getParameter', parameterMock);
|
|
50
|
+
utils.replaceWithNative(WebGL2RenderingContext.prototype, 'getParameter', parameterMock2);
|
|
51
|
+
utils.replaceWithNative(WebGLRenderingContext.prototype, 'getExtension', extensionMock);
|
|
52
|
+
utils.replaceWithNative(WebGL2RenderingContext.prototype, 'getExtension', extensionMock2);
|
|
53
|
+
} else {
|
|
54
|
+
WebGLRenderingContext.prototype.getParameter = parameterMock;
|
|
55
|
+
WebGL2RenderingContext.prototype.getParameter = parameterMock2;
|
|
56
|
+
WebGLRenderingContext.prototype.getExtension = extensionMock;
|
|
57
|
+
WebGL2RenderingContext.prototype.getExtension = extensionMock2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
thespis/js/worker.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Worker Proxy for Thespis Stealth */
|
|
2
|
+
const originalWorker = globalThis.Worker;
|
|
3
|
+
|
|
4
|
+
if (originalWorker) {
|
|
5
|
+
console.log('[Thespis] Worker proxy initialized');
|
|
6
|
+
|
|
7
|
+
const WorkerProxy = function(scriptURL, options) {
|
|
8
|
+
console.log('[Thespis] Worker constructor called with:', scriptURL, options);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const bundle = globalThis.__STEALTH_BUNDLE__;
|
|
12
|
+
if (!bundle) {
|
|
13
|
+
console.warn('[Thespis] __STEALTH_BUNDLE__ not found');
|
|
14
|
+
return new originalWorker(scriptURL, options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('[Thespis] Bundle found, length:', bundle.length);
|
|
18
|
+
|
|
19
|
+
// Handle string URLs (most common case)
|
|
20
|
+
if (typeof scriptURL === 'string') {
|
|
21
|
+
let finalURL = scriptURL;
|
|
22
|
+
|
|
23
|
+
// For relative/absolute URLs, make absolute
|
|
24
|
+
if (!scriptURL.startsWith('blob:') && !scriptURL.startsWith('data:')) {
|
|
25
|
+
finalURL = new URL(scriptURL, location.href).href;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('[Thespis] Creating worker with bundle injection');
|
|
29
|
+
|
|
30
|
+
// Create wrapper that injects bundle before loading original script
|
|
31
|
+
const wrapperCode = `
|
|
32
|
+
console.log('[Thespis Worker] Starting stealth injection');
|
|
33
|
+
${bundle}
|
|
34
|
+
console.log('[Thespis Worker] Stealth bundle executed');
|
|
35
|
+
try {
|
|
36
|
+
importScripts("${finalURL}");
|
|
37
|
+
console.log('[Thespis Worker] Original script loaded');
|
|
38
|
+
} catch(e) {
|
|
39
|
+
console.error("[Thespis Worker] importScripts failed:", e);
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const blob = new Blob([wrapperCode], { type: 'text/javascript' });
|
|
44
|
+
const blobURL = URL.createObjectURL(blob);
|
|
45
|
+
console.log('[Thespis] Created blob URL:', blobURL);
|
|
46
|
+
return new originalWorker(blobURL, options);
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('[Thespis] Worker proxy error:', e);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback
|
|
53
|
+
console.log('[Thespis] Using fallback - original worker');
|
|
54
|
+
return new originalWorker(scriptURL, options);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Copy prototype
|
|
58
|
+
WorkerProxy.prototype = originalWorker.prototype;
|
|
59
|
+
|
|
60
|
+
// Replace Worker constructor
|
|
61
|
+
if (globalThis.utils) {
|
|
62
|
+
utils.replaceWithNative(globalThis, 'Worker', WorkerProxy);
|
|
63
|
+
console.log('[Thespis] Worker masked with utils');
|
|
64
|
+
} else {
|
|
65
|
+
globalThis.Worker = WorkerProxy;
|
|
66
|
+
console.log('[Thespis] Worker replaced (no utils)');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
thespis/stealth.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from playwright.async_api import Page as PageAsync
|
|
5
|
+
from playwright.sync_api import Page as PageSync
|
|
6
|
+
|
|
7
|
+
# Determine the absolute path to the 'js' directory
|
|
8
|
+
SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "js")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_script(name: str) -> str:
|
|
12
|
+
"""Reads a JavaScript file from the 'js' directory."""
|
|
13
|
+
path = os.path.join(SCRIPTS_DIR, name)
|
|
14
|
+
try:
|
|
15
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
16
|
+
return f.read()
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
print(f"Warning: Script {name} not found in {SCRIPTS_DIR}")
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_stealth(page):
|
|
23
|
+
"""
|
|
24
|
+
Applies stealth JavaScript to the given page.
|
|
25
|
+
Injects evasion scripts to mask automation signals.
|
|
26
|
+
Works with both sync and async Page objects.
|
|
27
|
+
|
|
28
|
+
IMPORTANT: For best results, launch browser with:
|
|
29
|
+
browser = p.chromium.launch(
|
|
30
|
+
args=['--disable-blink-features=AutomationControlled']
|
|
31
|
+
)
|
|
32
|
+
This prevents navigator.webdriver from being set without causing "lie" detection.
|
|
33
|
+
|
|
34
|
+
The page.add_init_script calls run before page content loads,
|
|
35
|
+
because add_init_script is not awaitable/is consistent.
|
|
36
|
+
"""
|
|
37
|
+
# 0. Load all scripts and prepare bundle
|
|
38
|
+
# Order matters: utils first, then console/devtools detection bypasses
|
|
39
|
+
# NOTE: webdriver.js is NOT included - we use --disable-blink-features=AutomationControlled instead
|
|
40
|
+
# NOTE: offscreen_canvas.js is injected SEPARATELY before the bundle for early execution
|
|
41
|
+
scripts = [
|
|
42
|
+
"utils.js",
|
|
43
|
+
"console_cdp.js",
|
|
44
|
+
"devtools.js",
|
|
45
|
+
"screen.js",
|
|
46
|
+
"headless_fixes.js",
|
|
47
|
+
"chrome_app.js",
|
|
48
|
+
"permissions.js",
|
|
49
|
+
"user_agent.js",
|
|
50
|
+
"webgl.js",
|
|
51
|
+
]
|
|
52
|
+
bundle_parts = []
|
|
53
|
+
|
|
54
|
+
for s in scripts:
|
|
55
|
+
content = _read_script(s)
|
|
56
|
+
# Don't wrap in try/catch - let errors surface for debugging
|
|
57
|
+
bundle_parts.append(content)
|
|
58
|
+
|
|
59
|
+
full_bundle = "\n".join(bundle_parts)
|
|
60
|
+
|
|
61
|
+
# CRITICAL ORDER - Each script must inject at the right time:
|
|
62
|
+
# 0. FIRST: Patch OffscreenCanvas before ANYTHING else (for workers)
|
|
63
|
+
page.add_init_script(_read_script("offscreen_canvas.js"))
|
|
64
|
+
|
|
65
|
+
# 1. Second: Inject early worker interceptor (before ANY page script can save Worker ref)
|
|
66
|
+
page.add_init_script(_read_script("early_worker.js"))
|
|
67
|
+
|
|
68
|
+
# 2. Define global bundle string for Worker injection
|
|
69
|
+
bundle_json = json.dumps(full_bundle)
|
|
70
|
+
page.add_init_script(f"window.__STEALTH_BUNDLE__ = {bundle_json};")
|
|
71
|
+
|
|
72
|
+
# 3. Run the bundle in the Main Page
|
|
73
|
+
page.add_init_script(full_bundle)
|
|
74
|
+
|
|
75
|
+
# 4. Viewport adjustment
|
|
76
|
+
# Don't set viewport to exact screen size - this triggers hasVvpScreenRes
|
|
77
|
+
# Set to a common browser window size that's LESS than screen
|
|
78
|
+
# Common: 1366x768, 1440x900, 1536x864 (not fullscreen)
|
|
79
|
+
try:
|
|
80
|
+
page.set_viewport_size({"width": 1366, "height": 768})
|
|
81
|
+
except Exception:
|
|
82
|
+
# Can fail if context was created with viewport=None (no fixed size)
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def stealth_sync(page: PageSync):
|
|
87
|
+
"""Sync version of stealth"""
|
|
88
|
+
apply_stealth(page)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def stealth_async(page: PageAsync):
|
|
92
|
+
"""Async version of stealth"""
|
|
93
|
+
apply_stealth(page)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thespis
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Anti-detection plugin for Playwright automation
|
|
5
|
+
Project-URL: Homepage, https://github.com/jxlil/thespis
|
|
6
|
+
Author: Jalil SA
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: automation,bot-detection,playwright,stealth,web-scraping
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Requires-Dist: playwright>=1.30.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Thespis
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="assets/thespis.png" width="150" height="150" alt="Thespis" />
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://img.shields.io/badge/status-beta-orange" alt="Beta">
|
|
36
|
+
<img src="https://img.shields.io/badge/python-3.8+-blue" alt="Python">
|
|
37
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<strong>Make Playwright automation undetectable</strong>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
> **Note:** This project is currently in beta. Features are stable but may change based on feedback.
|
|
45
|
+
|
|
46
|
+
## Why Use Thespis?
|
|
47
|
+
|
|
48
|
+
Websites detect Playwright because of `navigator.webdriver` and other automation signals.
|
|
49
|
+
|
|
50
|
+
**Results:** Tested against [CreepJS](https://abrahamjuliot.github.io/creepjs/):
|
|
51
|
+
|
|
52
|
+
| Metric | Real Browser | With Thespis | Without Thespis |
|
|
53
|
+
| ----------------------- | ------------ | ------------ | --------------- |
|
|
54
|
+
| **Like Headless** | 25% | **31%** | 44% |
|
|
55
|
+
| **Headless Detection** | 0% | **0%** | 33% |
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Basic Example (Sync)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from playwright.sync_api import sync_playwright
|
|
63
|
+
from thespis import stealth_sync
|
|
64
|
+
|
|
65
|
+
with sync_playwright() as p:
|
|
66
|
+
# Launch browser with special flag (REQUIRED!)
|
|
67
|
+
browser = p.chromium.launch(
|
|
68
|
+
headless=False,
|
|
69
|
+
args=['--disable-blink-features=AutomationControlled']
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create page and apply stealth
|
|
73
|
+
page = browser.new_page()
|
|
74
|
+
stealth_sync(page)
|
|
75
|
+
|
|
76
|
+
page.goto("https://bot.sannysoft.com")
|
|
77
|
+
page.screenshot(path="test.png")
|
|
78
|
+
|
|
79
|
+
browser.close()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Important: Required Flag
|
|
83
|
+
|
|
84
|
+
**This flag is CRITICAL:**
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
args=['--disable-blink-features=AutomationControlled']
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Without it, `navigator.webdriver` stays `true` and detection fails.
|
|
91
|
+
|
|
92
|
+
## Docker (Production)
|
|
93
|
+
|
|
94
|
+
For servers without displays, use Docker with Xvfb:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Build image
|
|
98
|
+
docker-compose build
|
|
99
|
+
|
|
100
|
+
# Run test script
|
|
101
|
+
docker-compose run --rm thespis
|
|
102
|
+
|
|
103
|
+
# Run your own script
|
|
104
|
+
docker-compose run --rm thespis python your_script.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Why Docker?**
|
|
108
|
+
|
|
109
|
+
- Runs `headless=False` on servers (31% detection score)
|
|
110
|
+
- No visible windows (uses virtual display)
|
|
111
|
+
|
|
112
|
+
### Recommended Full Configuration
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Launch browser
|
|
116
|
+
browser = p.chromium.launch(
|
|
117
|
+
headless=False,
|
|
118
|
+
args=[
|
|
119
|
+
'--disable-blink-features=AutomationControlled',
|
|
120
|
+
'--disable-dev-shm-usage',
|
|
121
|
+
'--no-sandbox',
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Create realistic context
|
|
126
|
+
context = browser.new_context(
|
|
127
|
+
viewport={'width': 1366, 'height': 768},
|
|
128
|
+
user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
129
|
+
locale='en-US',
|
|
130
|
+
timezone_id='America/New_York',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
page = context.new_page()
|
|
134
|
+
stealth_sync(page)
|
|
135
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
thespis/__init__.py,sha256=h3X2XaFtUVI3caKvqt1_1ssvWXa_VptsAlfw-7Jgjsk,121
|
|
2
|
+
thespis/stealth.py,sha256=CTxyLktJR527I2Q7iqXVjQT6luSSRw0ciFYT-Huep_M,3184
|
|
3
|
+
thespis/js/chrome_app.js,sha256=tfhgVrcQivcO2gok2fuILitGtDAQIeAtojIWwLNRXSE,2238
|
|
4
|
+
thespis/js/console_cdp.js,sha256=WxlkqtfDXj5g6EiqRpq6S3y9yoFfLCWCTqRd4N7GjeQ,1794
|
|
5
|
+
thespis/js/devtools.js,sha256=fx-sucvym5w8Vc1EhIqnEi2tcNWCgWzL8-MlT4zxOvk,5461
|
|
6
|
+
thespis/js/early_worker.js,sha256=6rxzDk9LKZSWhTLUHt3SepWvfOTTwYj8lyqwBai6mbI,2136
|
|
7
|
+
thespis/js/headless_fixes.js,sha256=ePY4GS-iLCLfOMjXnzZ3cbpL2lcjk35PbcOU4OpPuf8,4189
|
|
8
|
+
thespis/js/offscreen_canvas.js,sha256=GWEwE9IUkpyIyb-UDP7bDJpnI17zc0mpZVbLz-pwZkw,2802
|
|
9
|
+
thespis/js/permissions.js,sha256=OcD7lFf9r64OA82aR-Ea3HJxCnv4Sn0Qpm8apYIew7o,388
|
|
10
|
+
thespis/js/screen.js,sha256=gRTKxVlXNNjT-CutnuFS1_uYtB-4kVAkJDsf4oucw2M,1609
|
|
11
|
+
thespis/js/stack_trace.js,sha256=qT_1Ev_TDdwTIrUg36YJN44PAkQUslX2hZ4Mv1QpFWE,2174
|
|
12
|
+
thespis/js/user_agent.js,sha256=z3WRPrjNsMUWI4M0eEjfIkc0yQn-jU2U5yXuEhd1oY4,2621
|
|
13
|
+
thespis/js/utils.js,sha256=CDbd1M2xnwoT2dWXEX-ZGRZJyOvKT6tnBzjwLzU4eww,2094
|
|
14
|
+
thespis/js/webdriver.js,sha256=_IfV3rwiU52EqOfOL-c0h77UOeQPEI-nactIlJSY0z4,1622
|
|
15
|
+
thespis/js/webgl.js,sha256=uZrekNLDjFGHGuvkoTPtHr80b5IkVcrO_mByYTJdRUk,2214
|
|
16
|
+
thespis/js/worker.js,sha256=bBO_ngypV3g-EjxS3os99XPNtJ-huySIaKubvCbhuW0,2760
|
|
17
|
+
thespis-0.1.0b1.dist-info/METADATA,sha256=-S9lb2-OKcKfsqLYZUiBgMlWNksFPA29VGmeB16V8pg,3718
|
|
18
|
+
thespis-0.1.0b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
thespis-0.1.0b1.dist-info/licenses/LICENSE,sha256=5QSnFBbFXKIjgTqB0lViq1b5merR6WkbJRrRhqGdIjc,1065
|
|
20
|
+
thespis-0.1.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jalil SA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|