fix: randomize hardware fingerprint values across browser sessions
Replace static stealthChromiumScripts and stealthFirefoxScripts slices with builder functions that accept hardware profile structs. Each browser session now randomly selects from a pool of 6 realistic profiles per engine, and Chromium connection stats receive per-session jitter (±20ms RTT, ±2 Mbps downlink). This prevents anti-bot systems from correlating sessions via identical WebGL, connection, mozInnerScreen, and hardwareConcurrency fingerprints. Closes #71 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -74,9 +74,9 @@ func initBrowser(opt BrowserOptions) (*browserInitResult, error) {
|
|||||||
initScripts = append(initScripts, stealthCommonScripts...)
|
initScripts = append(initScripts, stealthCommonScripts...)
|
||||||
switch opt.Browser {
|
switch opt.Browser {
|
||||||
case BrowserChromium:
|
case BrowserChromium:
|
||||||
initScripts = append(initScripts, stealthChromiumScripts...)
|
initScripts = append(initScripts, buildChromiumStealthScripts(randomChromiumProfile())...)
|
||||||
case BrowserFirefox:
|
case BrowserFirefox:
|
||||||
initScripts = append(initScripts, stealthFirefoxScripts...)
|
initScripts = append(initScripts, buildFirefoxStealthScripts(randomFirefoxProfile())...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
165
stealth.go
165
stealth.go
@@ -1,5 +1,10 @@
|
|||||||
package extractor
|
package extractor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
// stealthChromiumArgs are launch arguments that reduce automation detection for Chromium-based browsers.
|
// stealthChromiumArgs are launch arguments that reduce automation detection for Chromium-based browsers.
|
||||||
var stealthChromiumArgs = []string{
|
var stealthChromiumArgs = []string{
|
||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
@@ -39,10 +44,47 @@ var stealthCommonScripts = []string{
|
|||||||
})()`,
|
})()`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// stealthChromiumScripts are JavaScript snippets specific to Chromium-based browsers.
|
// chromiumHWProfile holds hardware fingerprint values for a Chromium browser session.
|
||||||
var stealthChromiumScripts = []string{
|
type chromiumHWProfile struct {
|
||||||
// Populate navigator.plugins with realistic Chromium entries so plugins.length > 0.
|
WebGLVendor string
|
||||||
`Object.defineProperty(navigator, 'plugins', {
|
WebGLRenderer string
|
||||||
|
ConnRTT int // base RTT in ms (jittered ±20 per session)
|
||||||
|
ConnDownlink float64 // base downlink in Mbps (jittered ±2 per session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chromiumHWProfiles is a pool of realistic Chromium hardware profiles.
|
||||||
|
// Index 0 matches the original hardcoded values.
|
||||||
|
var chromiumHWProfiles = []chromiumHWProfile{
|
||||||
|
{"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)", 50, 10},
|
||||||
|
{"Google Inc. (NVIDIA)", "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER, D3D11)", 30, 25},
|
||||||
|
{"Google Inc. (AMD)", "ANGLE (AMD, AMD Radeon RX 580, D3D11)", 100, 5},
|
||||||
|
{"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) UHD Graphics 770, OpenGL 4.5)", 50, 10},
|
||||||
|
{"Google Inc. (NVIDIA)", "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060, D3D11)", 25, 50},
|
||||||
|
{"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) Iris Xe Graphics, D3D11)", 75, 8},
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomChromiumProfile returns a randomly selected Chromium hardware profile.
|
||||||
|
func randomChromiumProfile() chromiumHWProfile {
|
||||||
|
return chromiumHWProfiles[rand.IntN(len(chromiumHWProfiles))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildChromiumStealthScripts returns Chromium stealth init scripts with the given hardware profile
|
||||||
|
// values templated into the WebGL and connection spoofing scripts. Connection RTT and downlink
|
||||||
|
// receive per-session jitter (±20ms RTT, ±2 Mbps downlink).
|
||||||
|
func buildChromiumStealthScripts(p chromiumHWProfile) []string {
|
||||||
|
// Apply jitter to connection stats.
|
||||||
|
rtt := p.ConnRTT + rand.IntN(41) - 20 // ±20ms
|
||||||
|
if rtt < 0 {
|
||||||
|
rtt = 0
|
||||||
|
}
|
||||||
|
downlink := p.ConnDownlink + (rand.Float64()*4 - 2) // ±2 Mbps
|
||||||
|
if downlink < 0.5 {
|
||||||
|
downlink = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{
|
||||||
|
// Populate navigator.plugins with realistic Chromium entries so plugins.length > 0.
|
||||||
|
`Object.defineProperty(navigator, 'plugins', {
|
||||||
get: () => {
|
get: () => {
|
||||||
const arr = [
|
const arr = [
|
||||||
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
||||||
@@ -56,8 +98,8 @@ var stealthChromiumScripts = []string{
|
|||||||
},
|
},
|
||||||
})`,
|
})`,
|
||||||
|
|
||||||
// Populate navigator.mimeTypes to match the fake Chromium plugins above.
|
// Populate navigator.mimeTypes to match the fake Chromium plugins above.
|
||||||
`Object.defineProperty(navigator, 'mimeTypes', {
|
`Object.defineProperty(navigator, 'mimeTypes', {
|
||||||
get: () => {
|
get: () => {
|
||||||
const arr = [
|
const arr = [
|
||||||
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
||||||
@@ -68,13 +110,13 @@ var stealthChromiumScripts = []string{
|
|||||||
},
|
},
|
||||||
})`,
|
})`,
|
||||||
|
|
||||||
// Provide window.chrome runtime stub (Chromium-only signal).
|
// Provide window.chrome runtime stub (Chromium-only signal).
|
||||||
`if (!window.chrome) {
|
`if (!window.chrome) {
|
||||||
window.chrome = { runtime: {} };
|
window.chrome = { runtime: {} };
|
||||||
}`,
|
}`,
|
||||||
|
|
||||||
// Add chrome.app, chrome.csi, and chrome.loadTimes stubs missing in headless.
|
// Add chrome.app, chrome.csi, and chrome.loadTimes stubs missing in headless.
|
||||||
`(function() {
|
`(function() {
|
||||||
if (!window.chrome) window.chrome = {};
|
if (!window.chrome) window.chrome = {};
|
||||||
if (!window.chrome.app) {
|
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' } };
|
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' } };
|
||||||
@@ -87,37 +129,37 @@ var stealthChromiumScripts = []string{
|
|||||||
}
|
}
|
||||||
})()`,
|
})()`,
|
||||||
|
|
||||||
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings.
|
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings.
|
||||||
`(function() {
|
fmt.Sprintf(`(function() {
|
||||||
const getParam = WebGLRenderingContext.prototype.getParameter;
|
const getParam = WebGLRenderingContext.prototype.getParameter;
|
||||||
WebGLRenderingContext.prototype.getParameter = function(param) {
|
WebGLRenderingContext.prototype.getParameter = function(param) {
|
||||||
if (param === 37445) return 'Google Inc. (Intel)';
|
if (param === 37445) return '%s';
|
||||||
if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)';
|
if (param === 37446) return '%s';
|
||||||
return getParam.call(this, param);
|
return getParam.call(this, param);
|
||||||
};
|
};
|
||||||
if (typeof WebGL2RenderingContext !== 'undefined') {
|
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||||
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
||||||
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
||||||
if (param === 37445) return 'Google Inc. (Intel)';
|
if (param === 37445) return '%s';
|
||||||
if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)';
|
if (param === 37446) return '%s';
|
||||||
return getParam2.call(this, param);
|
return getParam2.call(this, param);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})()`,
|
})()`, p.WebGLVendor, p.WebGLRenderer, p.WebGLVendor, p.WebGLRenderer),
|
||||||
|
|
||||||
// Stub navigator.connection (Network Information API) if missing (Chrome-only API).
|
// Stub navigator.connection (Network Information API) if missing (Chrome-only API).
|
||||||
`(function() {
|
fmt.Sprintf(`(function() {
|
||||||
if (!navigator.connection) {
|
if (!navigator.connection) {
|
||||||
Object.defineProperty(navigator, 'connection', {
|
Object.defineProperty(navigator, 'connection', {
|
||||||
get: function() {
|
get: function() {
|
||||||
return { effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null };
|
return { effectiveType: '4g', rtt: %d, downlink: %.1f, saveData: false, onchange: null };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})()`,
|
})()`, rtt, downlink),
|
||||||
|
|
||||||
// Remove CDP artifacts (window.cdc_* globals injected by Chrome DevTools Protocol).
|
// Remove CDP artifacts (window.cdc_* globals injected by Chrome DevTools Protocol).
|
||||||
`(function() {
|
`(function() {
|
||||||
for (var key in window) {
|
for (var key in window) {
|
||||||
if (key.match(/^cdc_/)) {
|
if (key.match(/^cdc_/)) {
|
||||||
delete window[key];
|
delete window[key];
|
||||||
@@ -125,8 +167,8 @@ var stealthChromiumScripts = []string{
|
|||||||
}
|
}
|
||||||
})()`,
|
})()`,
|
||||||
|
|
||||||
// Strip "HeadlessChrome" from navigator.userAgent if present.
|
// Strip "HeadlessChrome" from navigator.userAgent if present.
|
||||||
`(function() {
|
`(function() {
|
||||||
var ua = navigator.userAgent;
|
var ua = navigator.userAgent;
|
||||||
if (ua.indexOf('HeadlessChrome') !== -1) {
|
if (ua.indexOf('HeadlessChrome') !== -1) {
|
||||||
Object.defineProperty(navigator, 'userAgent', {
|
Object.defineProperty(navigator, 'userAgent', {
|
||||||
@@ -134,12 +176,40 @@ var stealthChromiumScripts = []string{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})()`,
|
})()`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stealthFirefoxScripts are JavaScript snippets specific to Firefox.
|
// firefoxHWProfile holds hardware fingerprint values for a Firefox browser session.
|
||||||
var stealthFirefoxScripts = []string{
|
type firefoxHWProfile struct {
|
||||||
// Harden navigator.webdriver for Firefox: ensure Object.getOwnPropertyDescriptor also returns undefined.
|
WebGLVendor string
|
||||||
`(function() {
|
WebGLRenderer string
|
||||||
|
MozInnerScreenX int
|
||||||
|
MozInnerScreenY int
|
||||||
|
HardwareConcurrency int
|
||||||
|
}
|
||||||
|
|
||||||
|
// firefoxHWProfiles is a pool of realistic Firefox hardware profiles.
|
||||||
|
// Index 0 matches the original hardcoded values.
|
||||||
|
var firefoxHWProfiles = []firefoxHWProfile{
|
||||||
|
{"Intel Open Source Technology Center", "Mesa DRI Intel(R) UHD Graphics 630", 8, 51, 4},
|
||||||
|
{"Intel Open Source Technology Center", "Mesa DRI Intel(R) HD Graphics 530", 0, 71, 8},
|
||||||
|
{"X.Org", "AMD Radeon RX 580 (polaris10, LLVM 15.0.7, DRM 3.49, 6.1.0-18-amd64)", 8, 51, 8},
|
||||||
|
{"Intel Open Source Technology Center", "Mesa DRI Intel(R) UHD Graphics 770", 0, 51, 16},
|
||||||
|
{"nouveau", "NV167", 8, 71, 4},
|
||||||
|
{"Intel", "Mesa Intel(R) Iris(R) Xe Graphics", 0, 51, 8},
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomFirefoxProfile returns a randomly selected Firefox hardware profile.
|
||||||
|
func randomFirefoxProfile() firefoxHWProfile {
|
||||||
|
return firefoxHWProfiles[rand.IntN(len(firefoxHWProfiles))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFirefoxStealthScripts returns Firefox stealth init scripts with the given hardware profile
|
||||||
|
// values templated into the WebGL, mozInnerScreen, and hardwareConcurrency spoofing scripts.
|
||||||
|
func buildFirefoxStealthScripts(p firefoxHWProfile) []string {
|
||||||
|
return []string{
|
||||||
|
// Harden navigator.webdriver for Firefox: ensure Object.getOwnPropertyDescriptor also returns undefined.
|
||||||
|
`(function() {
|
||||||
const proto = Object.getPrototypeOf(navigator);
|
const proto = Object.getPrototypeOf(navigator);
|
||||||
const origGetOwnPropDesc = Object.getOwnPropertyDescriptor;
|
const origGetOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
Object.getOwnPropertyDescriptor = function(obj, prop) {
|
Object.getOwnPropertyDescriptor = function(obj, prop) {
|
||||||
@@ -150,43 +220,43 @@ var stealthFirefoxScripts = []string{
|
|||||||
};
|
};
|
||||||
})()`,
|
})()`,
|
||||||
|
|
||||||
// Spoof WebGL renderer with Firefox-appropriate Mesa/Intel strings.
|
// Spoof WebGL renderer with Firefox-appropriate Mesa/driver strings.
|
||||||
`(function() {
|
fmt.Sprintf(`(function() {
|
||||||
const getParam = WebGLRenderingContext.prototype.getParameter;
|
const getParam = WebGLRenderingContext.prototype.getParameter;
|
||||||
WebGLRenderingContext.prototype.getParameter = function(param) {
|
WebGLRenderingContext.prototype.getParameter = function(param) {
|
||||||
if (param === 37445) return 'Intel Open Source Technology Center';
|
if (param === 37445) return '%s';
|
||||||
if (param === 37446) return 'Mesa DRI Intel(R) UHD Graphics 630';
|
if (param === 37446) return '%s';
|
||||||
return getParam.call(this, param);
|
return getParam.call(this, param);
|
||||||
};
|
};
|
||||||
if (typeof WebGL2RenderingContext !== 'undefined') {
|
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||||
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
||||||
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
||||||
if (param === 37445) return 'Intel Open Source Technology Center';
|
if (param === 37445) return '%s';
|
||||||
if (param === 37446) return 'Mesa DRI Intel(R) UHD Graphics 630';
|
if (param === 37446) return '%s';
|
||||||
return getParam2.call(this, param);
|
return getParam2.call(this, param);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})()`,
|
})()`, p.WebGLVendor, p.WebGLRenderer, p.WebGLVendor, p.WebGLRenderer),
|
||||||
|
|
||||||
// Spoof mozInnerScreenX/mozInnerScreenY which are 0 in headless Firefox.
|
// Spoof mozInnerScreenX/mozInnerScreenY which are 0 in headless Firefox.
|
||||||
`(function() {
|
fmt.Sprintf(`(function() {
|
||||||
if (window.mozInnerScreenX === 0) {
|
if (window.mozInnerScreenX === 0) {
|
||||||
Object.defineProperty(window, 'mozInnerScreenX', { get: () => 8 });
|
Object.defineProperty(window, 'mozInnerScreenX', { get: () => %d });
|
||||||
}
|
}
|
||||||
if (window.mozInnerScreenY === 0) {
|
if (window.mozInnerScreenY === 0) {
|
||||||
Object.defineProperty(window, 'mozInnerScreenY', { get: () => 51 });
|
Object.defineProperty(window, 'mozInnerScreenY', { get: () => %d });
|
||||||
}
|
}
|
||||||
})()`,
|
})()`, p.MozInnerScreenX, p.MozInnerScreenY),
|
||||||
|
|
||||||
// Normalize navigator.hardwareConcurrency (Firefox headless sometimes reports 2).
|
// Normalize navigator.hardwareConcurrency (Firefox headless sometimes reports 2).
|
||||||
`(function() {
|
fmt.Sprintf(`(function() {
|
||||||
if (navigator.hardwareConcurrency <= 2) {
|
if (navigator.hardwareConcurrency <= 2) {
|
||||||
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });
|
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => %d });
|
||||||
}
|
}
|
||||||
})()`,
|
})()`, p.HardwareConcurrency),
|
||||||
|
|
||||||
// Override navigator.plugins with Firefox-appropriate PDF.js entry.
|
// Override navigator.plugins with Firefox-appropriate PDF.js entry.
|
||||||
`Object.defineProperty(navigator, 'plugins', {
|
`Object.defineProperty(navigator, 'plugins', {
|
||||||
get: () => {
|
get: () => {
|
||||||
const arr = [
|
const arr = [
|
||||||
{ name: 'PDF.js', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
{ name: 'PDF.js', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
||||||
@@ -197,4 +267,5 @@ var stealthFirefoxScripts = []string{
|
|||||||
return arr;
|
return arr;
|
||||||
},
|
},
|
||||||
})`,
|
})`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
stealth_test.go
134
stealth_test.go
@@ -129,14 +129,16 @@ func TestStealthCommonScripts_Notification(t *testing.T) {
|
|||||||
// --- Chromium scripts ---
|
// --- Chromium scripts ---
|
||||||
|
|
||||||
func TestStealthChromiumScripts_Count(t *testing.T) {
|
func TestStealthChromiumScripts_Count(t *testing.T) {
|
||||||
if len(stealthChromiumScripts) != 8 {
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
t.Fatalf("expected 8 chromium stealth scripts, got %d", len(stealthChromiumScripts))
|
if len(scripts) != 8 {
|
||||||
|
t.Fatalf("expected 8 chromium stealth scripts, got %d", len(scripts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_Plugins(t *testing.T) {
|
func TestStealthChromiumScripts_Plugins(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "Chrome PDF Plugin") && strings.Contains(s, "navigator") && strings.Contains(s, "plugins") {
|
if strings.Contains(s, "Chrome PDF Plugin") && strings.Contains(s, "navigator") && strings.Contains(s, "plugins") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -148,8 +150,9 @@ func TestStealthChromiumScripts_Plugins(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_MimeTypes(t *testing.T) {
|
func TestStealthChromiumScripts_MimeTypes(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "mimeTypes") && strings.Contains(s, "application/pdf") {
|
if strings.Contains(s, "mimeTypes") && strings.Contains(s, "application/pdf") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -161,8 +164,9 @@ func TestStealthChromiumScripts_MimeTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_WindowChrome(t *testing.T) {
|
func TestStealthChromiumScripts_WindowChrome(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "window.chrome") && strings.Contains(s, "runtime") {
|
if strings.Contains(s, "window.chrome") && strings.Contains(s, "runtime") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -174,8 +178,9 @@ func TestStealthChromiumScripts_WindowChrome(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_ChromeApp(t *testing.T) {
|
func TestStealthChromiumScripts_ChromeApp(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") {
|
if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -187,8 +192,9 @@ func TestStealthChromiumScripts_ChromeApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) {
|
func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "37446") && strings.Contains(s, "ANGLE") {
|
if strings.Contains(s, "37446") && strings.Contains(s, "ANGLE") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -200,8 +206,9 @@ func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) {
|
func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") {
|
if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -213,8 +220,9 @@ func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_CDPCleanup(t *testing.T) {
|
func TestStealthChromiumScripts_CDPCleanup(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") {
|
if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -226,8 +234,9 @@ func TestStealthChromiumScripts_CDPCleanup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) {
|
func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") {
|
if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -241,14 +250,16 @@ func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) {
|
|||||||
// --- Firefox scripts ---
|
// --- Firefox scripts ---
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_Count(t *testing.T) {
|
func TestStealthFirefoxScripts_Count(t *testing.T) {
|
||||||
if len(stealthFirefoxScripts) != 5 {
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
t.Fatalf("expected 5 firefox stealth scripts, got %d", len(stealthFirefoxScripts))
|
if len(scripts) != 5 {
|
||||||
|
t.Fatalf("expected 5 firefox stealth scripts, got %d", len(scripts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) {
|
func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "getOwnPropertyDescriptor") && strings.Contains(s, "webdriver") {
|
if strings.Contains(s, "getOwnPropertyDescriptor") && strings.Contains(s, "webdriver") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -260,8 +271,9 @@ func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) {
|
func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "37446") && strings.Contains(s, "Mesa DRI") {
|
if strings.Contains(s, "37446") && strings.Contains(s, "Mesa DRI") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -273,8 +285,9 @@ func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) {
|
func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "mozInnerScreenX") && strings.Contains(s, "mozInnerScreenY") {
|
if strings.Contains(s, "mozInnerScreenX") && strings.Contains(s, "mozInnerScreenY") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -286,8 +299,9 @@ func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) {
|
func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "hardwareConcurrency") {
|
if strings.Contains(s, "hardwareConcurrency") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -299,8 +313,9 @@ func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) {
|
func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
found := false
|
found := false
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
if strings.Contains(s, "PDF.js") && strings.Contains(s, "plugins") {
|
if strings.Contains(s, "PDF.js") && strings.Contains(s, "plugins") {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -314,17 +329,19 @@ func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) {
|
|||||||
// --- Cross-category validation ---
|
// --- Cross-category validation ---
|
||||||
|
|
||||||
func TestStealthScripts_NoOverlap(t *testing.T) {
|
func TestStealthScripts_NoOverlap(t *testing.T) {
|
||||||
|
chromiumScripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
|
firefoxScripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
all := make(map[string]string) // script -> category
|
all := make(map[string]string) // script -> category
|
||||||
for _, s := range stealthCommonScripts {
|
for _, s := range stealthCommonScripts {
|
||||||
all[s] = "common"
|
all[s] = "common"
|
||||||
}
|
}
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range chromiumScripts {
|
||||||
if cat, ok := all[s]; ok {
|
if cat, ok := all[s]; ok {
|
||||||
t.Fatalf("chromium script also appears in %s category", cat)
|
t.Fatalf("chromium script also appears in %s category", cat)
|
||||||
}
|
}
|
||||||
all[s] = "chromium"
|
all[s] = "chromium"
|
||||||
}
|
}
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range firefoxScripts {
|
||||||
if cat, ok := all[s]; ok {
|
if cat, ok := all[s]; ok {
|
||||||
t.Fatalf("firefox script also appears in %s category", cat)
|
t.Fatalf("firefox script also appears in %s category", cat)
|
||||||
}
|
}
|
||||||
@@ -354,8 +371,9 @@ func TestStealthCommonScripts_NoFirefoxMarkers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) {
|
func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) {
|
||||||
|
scripts := buildChromiumStealthScripts(chromiumHWProfiles[0])
|
||||||
firefoxMarkers := []string{"mozInnerScreen", "Mesa DRI", "PDF.js"}
|
firefoxMarkers := []string{"mozInnerScreen", "Mesa DRI", "PDF.js"}
|
||||||
for _, s := range stealthChromiumScripts {
|
for _, s := range scripts {
|
||||||
for _, marker := range firefoxMarkers {
|
for _, marker := range firefoxMarkers {
|
||||||
if strings.Contains(s, marker) {
|
if strings.Contains(s, marker) {
|
||||||
t.Fatalf("chromium script contains Firefox-specific marker %q", marker)
|
t.Fatalf("chromium script contains Firefox-specific marker %q", marker)
|
||||||
@@ -365,8 +383,9 @@ func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStealthFirefoxScripts_NoChromiumMarkers(t *testing.T) {
|
func TestStealthFirefoxScripts_NoChromiumMarkers(t *testing.T) {
|
||||||
|
scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0])
|
||||||
chromiumMarkers := []string{"window.chrome", "chrome.app", "chrome.csi", "chrome.loadTimes", "HeadlessChrome", "cdc_", "Chrome PDF Plugin", "ANGLE"}
|
chromiumMarkers := []string{"window.chrome", "chrome.app", "chrome.csi", "chrome.loadTimes", "HeadlessChrome", "cdc_", "Chrome PDF Plugin", "ANGLE"}
|
||||||
for _, s := range stealthFirefoxScripts {
|
for _, s := range scripts {
|
||||||
for _, marker := range chromiumMarkers {
|
for _, marker := range chromiumMarkers {
|
||||||
if strings.Contains(s, marker) {
|
if strings.Contains(s, marker) {
|
||||||
t.Fatalf("firefox script contains Chromium-specific marker %q", marker)
|
t.Fatalf("firefox script contains Chromium-specific marker %q", marker)
|
||||||
@@ -439,3 +458,76 @@ func TestMergeOptions_ExplicitUAPreserved(t *testing.T) {
|
|||||||
t.Fatalf("expected explicit UA %q preserved, got %q", customUA, got.UserAgent)
|
t.Fatalf("expected explicit UA %q preserved, got %q", customUA, got.UserAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Hardware profile pools ---
|
||||||
|
|
||||||
|
func TestChromiumHWProfiles_NotEmpty(t *testing.T) {
|
||||||
|
if len(chromiumHWProfiles) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 chromium hardware profiles, got %d", len(chromiumHWProfiles))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirefoxHWProfiles_NotEmpty(t *testing.T) {
|
||||||
|
if len(firefoxHWProfiles) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 firefox hardware profiles, got %d", len(firefoxHWProfiles))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildChromiumStealthScripts_ProfileValues(t *testing.T) {
|
||||||
|
p := chromiumHWProfiles[1] // NVIDIA profile
|
||||||
|
scripts := buildChromiumStealthScripts(p)
|
||||||
|
joined := strings.Join(scripts, "\n")
|
||||||
|
if !strings.Contains(joined, p.WebGLVendor) {
|
||||||
|
t.Fatalf("expected chromium scripts to contain vendor %q", p.WebGLVendor)
|
||||||
|
}
|
||||||
|
if !strings.Contains(joined, p.WebGLRenderer) {
|
||||||
|
t.Fatalf("expected chromium scripts to contain renderer %q", p.WebGLRenderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFirefoxStealthScripts_ProfileValues(t *testing.T) {
|
||||||
|
p := firefoxHWProfiles[2] // AMD profile
|
||||||
|
scripts := buildFirefoxStealthScripts(p)
|
||||||
|
joined := strings.Join(scripts, "\n")
|
||||||
|
if !strings.Contains(joined, p.WebGLVendor) {
|
||||||
|
t.Fatalf("expected firefox scripts to contain vendor %q", p.WebGLVendor)
|
||||||
|
}
|
||||||
|
if !strings.Contains(joined, p.WebGLRenderer) {
|
||||||
|
t.Fatalf("expected firefox scripts to contain renderer %q", p.WebGLRenderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildChromiumStealthScripts_ConnectionJitter(t *testing.T) {
|
||||||
|
p := chromiumHWProfiles[0]
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for range 50 {
|
||||||
|
scripts := buildChromiumStealthScripts(p)
|
||||||
|
// The connection script is at index 5.
|
||||||
|
seen[scripts[5]] = true
|
||||||
|
}
|
||||||
|
if len(seen) < 2 {
|
||||||
|
t.Fatal("expected connection script to vary across calls due to jitter, but all 50 were identical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChromiumHWProfiles_NoSingleQuotes(t *testing.T) {
|
||||||
|
for i, p := range chromiumHWProfiles {
|
||||||
|
if strings.Contains(p.WebGLVendor, "'") {
|
||||||
|
t.Fatalf("chromium profile %d vendor contains single quote (breaks JS)", i)
|
||||||
|
}
|
||||||
|
if strings.Contains(p.WebGLRenderer, "'") {
|
||||||
|
t.Fatalf("chromium profile %d renderer contains single quote (breaks JS)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirefoxHWProfiles_NoSingleQuotes(t *testing.T) {
|
||||||
|
for i, p := range firefoxHWProfiles {
|
||||||
|
if strings.Contains(p.WebGLVendor, "'") {
|
||||||
|
t.Fatalf("firefox profile %d vendor contains single quote (breaks JS)", i)
|
||||||
|
}
|
||||||
|
if strings.Contains(p.WebGLRenderer, "'") {
|
||||||
|
t.Fatalf("firefox profile %d renderer contains single quote (breaks JS)", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user