Merge pull request 'fix: enhance stealth mode with additional anti-detection' (#59) from fix/enhanced-stealth-mode into main
This commit was merged in pull request #59.
This commit is contained in:
@@ -92,6 +92,9 @@ func initBrowser(opt BrowserOptions) (*browserInitResult, error) {
|
|||||||
if len(launchArgs) > 0 {
|
if len(launchArgs) > 0 {
|
||||||
launchOpts.Args = launchArgs
|
launchOpts.Args = launchArgs
|
||||||
}
|
}
|
||||||
|
if stealth && opt.Browser == BrowserChromium && headless {
|
||||||
|
launchOpts.Channel = playwright.String("chromium")
|
||||||
|
}
|
||||||
browser, err = bt.Launch(launchOpts)
|
browser, err = bt.Launch(launchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to launch browser: %w", err)
|
return nil, fmt.Errorf("failed to launch browser: %w", err)
|
||||||
|
|||||||
84
stealth.go
84
stealth.go
@@ -47,4 +47,88 @@ var stealthInitScripts = []string{
|
|||||||
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
|
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
|
||||||
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight });
|
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight });
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
|
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint.
|
||||||
|
`(function() {
|
||||||
|
const getParam = WebGLRenderingContext.prototype.getParameter;
|
||||||
|
WebGLRenderingContext.prototype.getParameter = function(param) {
|
||||||
|
if (param === 37445) return 'Google Inc. (Intel)';
|
||||||
|
if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)';
|
||||||
|
return getParam.call(this, param);
|
||||||
|
};
|
||||||
|
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||||
|
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
||||||
|
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
||||||
|
if (param === 37445) return 'Google Inc. (Intel)';
|
||||||
|
if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)';
|
||||||
|
return getParam2.call(this, param);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Add chrome.app, chrome.csi, and chrome.loadTimes stubs missing in headless.
|
||||||
|
`(function() {
|
||||||
|
if (!window.chrome) window.chrome = {};
|
||||||
|
if (!window.chrome.app) {
|
||||||
|
window.chrome.app = { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } };
|
||||||
|
}
|
||||||
|
if (!window.chrome.csi) {
|
||||||
|
window.chrome.csi = function() { return { startE: Date.now(), onloadT: Date.now(), pageT: 0, tran: 15 }; };
|
||||||
|
}
|
||||||
|
if (!window.chrome.loadTimes) {
|
||||||
|
window.chrome.loadTimes = function() { return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: Date.now() / 1000, finishLoadTime: Date.now() / 1000, firstPaintAfterLoadTime: 0, firstPaintTime: Date.now() / 1000, navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000, startLoadTime: Date.now() / 1000, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Override navigator.permissions.query to return "denied" for notifications.
|
||||||
|
`(function() {
|
||||||
|
if (navigator.permissions && navigator.permissions.query) {
|
||||||
|
const origQuery = navigator.permissions.query.bind(navigator.permissions);
|
||||||
|
navigator.permissions.query = function(desc) {
|
||||||
|
if (desc && desc.name === 'notifications') {
|
||||||
|
return Promise.resolve({ state: 'denied', onchange: null });
|
||||||
|
}
|
||||||
|
return origQuery(desc);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Stub Notification constructor if missing (headless may lack it).
|
||||||
|
`(function() {
|
||||||
|
if (typeof Notification === 'undefined') {
|
||||||
|
window.Notification = function() {};
|
||||||
|
Notification.permission = 'denied';
|
||||||
|
Notification.requestPermission = function() { return Promise.resolve('denied'); };
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Stub navigator.connection (Network Information API) if missing.
|
||||||
|
`(function() {
|
||||||
|
if (!navigator.connection) {
|
||||||
|
Object.defineProperty(navigator, 'connection', {
|
||||||
|
get: function() {
|
||||||
|
return { effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Remove CDP artifacts (window.cdc_* globals injected by Chrome DevTools Protocol).
|
||||||
|
`(function() {
|
||||||
|
for (var key in window) {
|
||||||
|
if (key.match(/^cdc_/)) {
|
||||||
|
delete window[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
|
|
||||||
|
// Strip "HeadlessChrome" from navigator.userAgent if present.
|
||||||
|
`(function() {
|
||||||
|
var ua = navigator.userAgent;
|
||||||
|
if (ua.indexOf('HeadlessChrome') !== -1) {
|
||||||
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
|
get: function() { return ua.replace('HeadlessChrome', 'Chrome'); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})()`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package extractor
|
package extractor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,3 +71,100 @@ func TestStealthInitScripts(t *testing.T) {
|
|||||||
t.Fatal("expected at least one stealth init script")
|
t.Fatal("expected at least one stealth init script")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_Count(t *testing.T) {
|
||||||
|
if len(stealthInitScripts) != 12 {
|
||||||
|
t.Fatalf("expected 12 stealth init scripts, got %d", len(stealthInitScripts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_WebGLSpoof(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "SwiftShader") || strings.Contains(s, "UNMASKED_RENDERER") || strings.Contains(s, "37446") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that spoofs WebGL renderer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_ChromeApp(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that stubs chrome.app, chrome.csi, and chrome.loadTimes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_PermissionsQuery(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "permissions.query") && strings.Contains(s, "notifications") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that overrides permissions.query for notifications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_Notification(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "Notification") && strings.Contains(s, "requestPermission") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that stubs Notification constructor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_NavigatorConnection(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that stubs navigator.connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_CDPCleanup(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that cleans up CDP artifacts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStealthInitScripts_UserAgentStrip(t *testing.T) {
|
||||||
|
found := false
|
||||||
|
for _, s := range stealthInitScripts {
|
||||||
|
if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected a stealth script that strips HeadlessChrome from user agent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user