diff --git a/crates/obscura-browser/src/page.rs b/crates/obscura-browser/src/page.rs index 51e75ea..44fb44f 100644 --- a/crates/obscura-browser/src/page.rs +++ b/crates/obscura-browser/src/page.rs @@ -287,9 +287,11 @@ impl Page { if self.stealth_client.is_some() { rt.set_stealth(true); rt.set_user_agent(obscura_net::STEALTH_USER_AGENT); - // Match platform to the Linux UA so navigator.platform and - // userAgentData.platform are consistent with navigator.userAgent. - rt.set_platform("Linux x86_64", "Linux", ""); + rt.set_platform( + obscura_net::STEALTH_NAVIGATOR_PLATFORM, + obscura_net::STEALTH_UA_PLATFORM, + obscura_net::STEALTH_UA_PLATFORM_VERSION, + ); } else { if let Ok(ua) = self.http_client.user_agent.try_read() { rt.set_user_agent(&ua); diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index c1fb240..a4e2d3a 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -154,7 +154,9 @@ function _fpNoise(x, y, channel) { var _fpCache = null; function _getFp() { if (_fpCache) return _fpCache; - const isMac = (globalThis.__obscura_ua_platform || 'Windows') === 'macOS'; + const _uaPlat = globalThis.__obscura_ua_platform || 'Windows'; + const isMac = _uaPlat === 'macOS'; + const isLinux = _uaPlat === 'Linux'; const gpuPool = isMac ? [ 'ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)', 'ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)', @@ -162,6 +164,14 @@ function _getFp() { 'ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version)', 'ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)', 'ANGLE (Intel Inc., ANGLE Metal Renderer: Intel(R) Iris(TM) Plus Graphics, Unspecified Version)', + ] : isLinux ? [ + 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)', + 'ANGLE (Intel, Mesa Intel(R) Iris(R) Xe Graphics (TGL GT2), OpenGL 4.6)', + 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (RPL-S), OpenGL 4.6)', + 'ANGLE (AMD, AMD Radeon RX 580 (polaris10, LLVM 15.0.7, DRM 3.54, LLVM 15.0.7), OpenGL 4.6)', + 'ANGLE (AMD, AMD Radeon RX 6700 XT (navi22, LLVM 16.0.6, DRM 3.54, LLVM 16.0.6), OpenGL 4.6)', + 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 OpenGL 4.6)', + 'ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 OpenGL 4.6)', ] : [ 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)', 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)', @@ -180,6 +190,10 @@ function _getFp() { 'Google Inc. (Apple)','Google Inc. (Apple)','Google Inc. (Apple)', 'Google Inc. (Apple)','Google Inc. (Apple)', 'Google Inc. (Intel Inc.)', + ] : isLinux ? [ + 'Google Inc. (Intel)','Google Inc. (Intel)','Google Inc. (Intel)', + 'Google Inc. (AMD)','Google Inc. (AMD)', + 'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)', ] : [ 'Google Inc. (NVIDIA)','Google Inc. (NVIDIA)','Google Inc. (NVIDIA)', 'Google Inc. (Intel)','Google Inc. (Intel)', @@ -2612,8 +2626,35 @@ globalThis.NetworkInformation = NetworkInformation; globalThis.ContentIndex = class ContentIndex {}; +function _chromeMajor() { + var m = (globalThis.__obscura_ua || '').match(/Chrome\/(\d+)/); + return m ? (m[1] | 0) : 145; +} +// Chromium derives the sec-ch-ua GREASE brand, version, and brand order +// deterministically from the Chrome major version +// (components/embedder_support/user_agent_utils.cc). Replicating it keeps +// sec-ch-ua and userAgentData exact for every profile version rather than +// hardcoding one static token. +var _GREASE_CHARS = [' ', '(', ':', '-', '.', '/', ')', ';', '=', '?', '_']; +var _GREASE_VER = ['8', '99', '24']; +var _BRAND_PERMS = [[0,1,2],[0,2,1],[1,0,2],[1,2,0],[2,0,1],[2,1,0]]; +function _uaBrands() { + var seed = _chromeMajor(); + var grease = { + brand: 'Not' + _GREASE_CHARS[seed % 11] + 'A' + _GREASE_CHARS[(seed + 1) % 11] + 'Brand', + version: _GREASE_VER[seed % 3], + }; + var ordered = [ + grease, + {brand: 'Chromium', version: String(seed)}, + {brand: 'Google Chrome', version: String(seed)}, + ]; + var p = _BRAND_PERMS[seed % 6]; + return [ordered[p[0]], ordered[p[1]], ordered[p[2]]]; +} + globalThis.navigator = { - get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; }, + get userAgent() { return globalThis.__obscura_ua || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; }, get appVersion() { return this.userAgent.replace('Mozilla/', ''); }, language: "en-US", languages: ["en-US","en"], get platform() { return globalThis.__obscura_platform || "Win32"; }, onLine: true, cookieEnabled: true, hardwareConcurrency: 8, @@ -2643,24 +2684,22 @@ globalThis.navigator = { return m; }, userAgentData: { - brands: [ - {brand: "Google Chrome", version: "145"}, - {brand: "Chromium", version: "145"}, - {brand: "Not;A=Brand", version: "24"}, - ], mobile: false, + get brands() { return _uaBrands(); }, get platform() { return globalThis.__obscura_ua_platform || "Windows"; }, getHighEntropyValues(hints) { + var brands = _uaBrands(); return Promise.resolve({ architecture: "x86", bitness: "64", - brands: [{brand:"Google Chrome",version:"145"},{brand:"Chromium",version:"145"},{brand:"Not;A=Brand",version:"24"}], - fullVersionList: [{brand:"Google Chrome",version:"145.0.0.0"},{brand:"Chromium",version:"145.0.0.0"},{brand:"Not;A=Brand",version:"24.0.0.0"}], + brands: brands, + fullVersionList: brands.map(function(b) { return {brand: b.brand, version: b.version + ".0.0.0"}; }), mobile: false, model: "", platform: globalThis.__obscura_ua_platform || "Windows", platformVersion: globalThis.__obscura_ua_platform_version || "15.0.0", - uaFullVersion: "145.0.0.0", + uaFullVersion: _chromeMajor() + ".0.0.0", + wow64: false, }); }, toJSON() { return {brands:this.brands,mobile:this.mobile,platform:this.platform}; }, @@ -6722,29 +6761,9 @@ globalThis.__obscura_init = function() { }; globalThis.Notification.permission = "default"; - if (globalThis.__obscura_stealth) { - var _stealthBrands = [ - {brand: "Not:A-Brand", version: "99"}, - {brand: "Google Chrome", version: "145"}, - {brand: "Chromium", version: "145"}, - ]; - globalThis.navigator.userAgentData.brands = _stealthBrands; - globalThis.navigator.userAgentData.getHighEntropyValues = function(hints) { - return Promise.resolve({ - architecture: "x86", bitness: "64", - brands: _stealthBrands, - fullVersionList: [ - {brand:"Not:A-Brand",version:"99.0.0.0"}, - {brand:"Google Chrome",version:"145.0.0.0"}, - {brand:"Chromium",version:"145.0.0.0"}, - ], - mobile: false, model: "", - platform: "Windows", - platformVersion: "10.0.0", - uaFullVersion: "145.0.0.0", - }); - }; - } + // userAgentData brands and getHighEntropyValues now derive the Chrome + // version from navigator.userAgent and read the platform from the page + // globals, so every stealth surface agrees without a per-mode override. // Hide internals (_*, obscura, Obscura). The set of keys is static at // snapshot-build time, so we precompute it ONCE below (after this diff --git a/crates/obscura-net/src/lib.rs b/crates/obscura-net/src/lib.rs index e373575..f9840ad 100644 --- a/crates/obscura-net/src/lib.rs +++ b/crates/obscura-net/src/lib.rs @@ -19,4 +19,7 @@ pub use encoding::{ pub use robots::RobotsCache; pub use blocklist::is_blocked as is_tracker_blocked; #[cfg(feature = "stealth")] -pub use wreq_client::{StealthHttpClient, STEALTH_USER_AGENT}; +pub use wreq_client::{ + StealthHttpClient, STEALTH_NAVIGATOR_PLATFORM, STEALTH_UA_PLATFORM, + STEALTH_UA_PLATFORM_VERSION, STEALTH_USER_AGENT, +}; diff --git a/crates/obscura-net/src/wreq_client.rs b/crates/obscura-net/src/wreq_client.rs index e0405d4..1361640 100644 --- a/crates/obscura-net/src/wreq_client.rs +++ b/crates/obscura-net/src/wreq_client.rs @@ -19,7 +19,18 @@ use crate::client::{Response, ObscuraNetError}; #[cfg(feature = "stealth")] pub const STEALTH_USER_AGENT: &str = - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"; + +// The wreq emulation (Profile::Chrome145, Platform::Windows) sends this exact +// UA and sec-ch-ua-platform "Windows" on the wire. navigator has to report the +// same identity, otherwise the TLS/HTTP layer and the JS layer disagree and a +// site cross-checks the mismatch as a bot signal. +#[cfg(feature = "stealth")] +pub const STEALTH_NAVIGATOR_PLATFORM: &str = "Win32"; +#[cfg(feature = "stealth")] +pub const STEALTH_UA_PLATFORM: &str = "Windows"; +#[cfg(feature = "stealth")] +pub const STEALTH_UA_PLATFORM_VERSION: &str = "15.0.0"; #[cfg(feature = "stealth")] pub struct StealthHttpClient { @@ -38,7 +49,7 @@ impl StealthHttpClient { pub fn with_proxy(cookie_jar: Arc, proxy_url: Option<&str>) -> Self { let emulation_opts = wreq_util::Emulation::builder() .profile(wreq_util::Profile::Chrome145) - .platform(wreq_util::Platform::Linux) + .platform(wreq_util::Platform::Windows) .build(); let mut builder = wreq::Client::builder()