pub mod cdp;
pub mod error;
pub mod identity;
pub mod local_proxy_server;
pub mod preset;
pub mod script;
pub mod tz;
pub mod ua;
use error::IdentityError;
use local_proxy_server::start_overlay;
use rustenium::browsers::{
BidiBrowser,
chrome::browser::{ChromeBrowser, ChromeConfig},
};
pub use error::IdentityError as Error;
pub use identity::*;
pub struct IdentityConfig {
pub identity: Identity,
pub chrome: ChromeConfig,
}
impl From<Identity> for IdentityConfig {
fn from(identity: Identity) -> Self {
Self {
identity,
chrome: ChromeConfig {
enable_bidi: false,
enable_cdp: true,
..Default::default()
},
}
}
}
impl IdentityConfig {
pub fn new(identity: Identity, chrome: ChromeConfig) -> Self {
Self { identity, chrome }
}
}
async fn read_chromium_version(browser: &mut ChromeBrowser) -> Option<Vec<u16>> {
const EXPR: &str = r#"(async () => {
try {
const d = navigator.userAgentData;
if (d && d.getHighEntropyValues) {
const v = await d.getHighEntropyValues(['uaFullVersion']);
if (v && v.uaFullVersion) return v.uaFullVersion;
}
} catch (e) {}
const m = navigator.userAgent.match(/Chrome\/([\d.]+)/);
return m ? m[1] : '';
})()"#;
let result =
rustenium::browsers::cdp_browser::CdpBrowser::evaluate_script(browser, EXPR, true)
.await
.ok()?;
let version = result.result.value.as_ref()?.as_str()?.to_string();
ua::parse_version_parts(&version)
}
async fn read_gpu_strings(browser: &mut ChromeBrowser) -> Option<(String, String)> {
const EXPR: &str = r#"(() => {
try {
const gl = document.createElement('canvas').getContext('webgl');
const ext = gl && gl.getExtension('WEBGL_debug_renderer_info');
if (!ext) return '[]';
return JSON.stringify([
gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
]);
} catch (e) { return '[]'; }
})()"#;
let result =
rustenium::browsers::cdp_browser::CdpBrowser::evaluate_script(browser, EXPR, false)
.await
.ok()?;
let json = result.result.value.as_ref()?.as_str()?;
let pair: Vec<String> = serde_json::from_str(json).ok()?;
let [vendor, renderer] = <[String; 2]>::try_from(pair).ok()?;
if vendor.is_empty() || renderer.is_empty() {
return None;
}
Some((vendor, renderer))
}
fn host_has_gpu() -> bool {
if !cfg!(target_os = "linux") {
return true;
}
std::fs::read_dir("/dev/dri").is_ok_and(|nodes| {
nodes
.flatten()
.any(|n| n.file_name().to_string_lossy().starts_with("renderD"))
})
}
const SOFTWARE_GL_FLAGS: [&str; 3] = ["--use-gl=angle", "--use-angle=gl", "--ignore-gpu-blocklist"];
pub struct IdentitySession {
identity: Identity,
browser: ChromeBrowser,
}
impl IdentitySession {
pub async fn launch(config: impl Into<IdentityConfig>) -> Result<Self, IdentityError> {
let config = config.into();
let geo = tz::resolve_geo(
config.identity.timezone.as_deref(),
config.identity.proxy.as_deref(),
)
.await?;
let timezone = geo.timezone.clone();
let mut chrome_config = config.chrome;
chrome_config.enable_bidi = false;
chrome_config.enable_cdp = true;
let mut flags = chrome_config.browser_flags.take().unwrap_or_default();
flags.push("--disable-blink-features=AutomationControlled".to_string());
if !host_has_gpu() {
tracing::warn!(
"no GPU on this host; pointing ANGLE at the system GL. The limits, \
extension list and precision formats a page reads will describe a \
software renderer — only the vendor/renderer strings are spoofed."
);
flags.extend(SOFTWARE_GL_FLAGS.iter().map(|f| f.to_string()));
}
if let Some(lang) = config.identity.language.first() {
flags.push(format!("--lang={lang}"));
}
chrome_config.browser_flags = Some(flags);
if let Some(ref proxy_url) = config.identity.proxy {
if !proxy_url.is_empty() {
let local_addr = start_overlay(proxy_url)
.await
.map_err(|e| IdentityError::ProxyError(e.to_string()))?;
let mut flags = chrome_config.browser_flags.take().unwrap_or_default();
flags.push(format!(
"--proxy-server=http://127.0.0.1:{}",
local_addr.port()
));
chrome_config.browser_flags = Some(flags);
}
}
let mut browser = ChromeBrowser::new(chrome_config).await;
if matches!(config.identity.browser, Browser::Chrome | Browser::Edge)
&& !matches!(config.identity.os, Os::Ios)
{
match read_chromium_version(&mut browser).await {
Some(version) if version != config.identity.browser_version => tracing::warn!(
configured = ?config.identity.browser_version,
running = ?version,
"persona browser_version does not match the running Chromium"
),
Some(_) => {}
None => tracing::warn!("could not read the running Chromium version"),
}
}
match read_gpu_strings(&mut browser).await {
Some((_, renderer)) if renderer != config.identity.gpu.webgl_renderer => {
tracing::warn!(
host_renderer = %renderer,
persona_renderer = %config.identity.gpu.webgl_renderer,
"host GPU differs from the persona's; the WebGL capability \
profile comes from the host and will not match the claimed card"
)
}
Some(_) => {}
None => tracing::warn!("could not read the real WebGL vendor/renderer"),
}
cdp::apply_identity(&mut browser, &config.identity, &timezone).await?;
Ok(Self {
identity: config.identity,
browser,
})
}
pub fn browser(&self) -> &ChromeBrowser {
&self.browser
}
pub fn browser_mut(&mut self) -> &mut ChromeBrowser {
&mut self.browser
}
pub fn identity(&self) -> &Identity {
&self.identity
}
pub async fn close(self) -> bool {
self.browser.close().await.map_err(|_| false).is_ok()
}
}