mod auto_resolve;
mod browser;
#[cfg(feature = "control")]
mod control;
mod geo;
mod host;
mod profile;
mod proxy;
mod randomize;
mod runtime;
mod screen;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use rand::seq::SliceRandom;
use serde_json::Value;
pub use auto_resolve::{has_auto_fields, resolve_auto_fields};
pub use browser::{Browser, BrowserSession, LaunchOptions, WebRtcMode};
#[cfg(feature = "control")]
pub use chromiumoxide;
#[cfg(feature = "control")]
pub use control::Session;
pub use geo::{geo_check_via, GeoInfo};
pub use host::{host_logical_cores, host_ram_bucket_gb, host_ram_gb, host_screen_size, Size};
pub use profile::{apply_engine_version, user_data_dir, FingerprintLibrary, Profile};
pub use proxy::{parse_proxy, probe_udp, proxy_to_arg, ParsedProxy, ProxyScheme};
pub use randomize::{
mac_hw_configs, randomize_hardware, randomize_platform_version, LINUX_PLATFORM_VERSIONS,
MACOS_PLATFORM_VERSIONS, WINDOWS_PLATFORM_VERSIONS, X86_CORES,
};
pub use runtime::{
default_cache_dir, host_spec, Archive, HostSpec, ProgressCb, Runtime, CHROMIUM_VERSION,
PUB_BASE,
};
pub use screen::{apply_screen_strategy, default_screen_mode_for, ScreenStrategy};
#[derive(Default)]
pub struct ShardXOptions {
pub cache_dir: Option<PathBuf>,
pub profiles_dir: Option<PathBuf>,
pub progress: Option<ProgressCb>,
}
#[derive(Debug, Clone)]
pub struct ProxyCheckResult {
pub udp_ms: Option<u128>,
pub geo: GeoInfo,
pub would_enable_quic: bool,
pub would_set_webrtc: WebRtcMode,
}
fn new_profile_id() -> String {
format!("{:032x}", rand::random::<u128>())
}
pub struct ShardX {
pub runtime: Arc<Runtime>,
library: FingerprintLibrary,
browser: Browser,
}
impl ShardX {
pub fn new(opts: ShardXOptions) -> Result<Self> {
let runtime = Arc::new(Runtime::new(
opts.cache_dir,
opts.profiles_dir,
opts.progress,
)?);
Ok(Self {
library: FingerprintLibrary::new(runtime.clone()),
browser: Browser::new(runtime.clone()),
runtime,
})
}
pub async fn list_profiles(&self, platform: Option<&str>) -> Result<Vec<String>> {
self.runtime.install(false).await?;
Ok(match platform {
Some(p) => self.library.filter(Some(p)),
None => self.library.ids(),
})
}
pub async fn random_profile(&self, platform: Option<&str>) -> Result<Profile> {
let ids = self.list_profiles(platform).await?;
let id = ids.choose(&mut rand::thread_rng()).ok_or_else(|| {
anyhow!(
"No bundled profiles found{}.",
platform.map(|p| format!(" for platform={p}")).unwrap_or_default()
)
})?;
self.library.load(id)
}
pub async fn create_profile(&self, template: Option<&str>) -> Result<Profile> {
self.runtime.install(false).await?;
let mut config = match template {
Some(id) => self.library.load(id)?.config,
None => self.random_profile(None).await?.config,
};
let id = new_profile_id();
randomize_hardware(&mut config, Some(&id));
randomize_platform_version(&mut config);
let profile = Profile::new(config, Some(id));
self.save_profile(&profile)?;
Ok(profile)
}
pub fn save_profile(&self, profile: &Profile) -> Result<()> {
let path = self.profile_json_path(&profile.id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, serde_json::to_string_pretty(&profile.config)?)?;
Ok(())
}
pub fn open_profile(&self, id: &str) -> Result<Profile> {
let path = self.profile_json_path(id);
if !path.exists() {
return Err(anyhow!("saved profile '{id}' not found"));
}
let config: Value = serde_json::from_str(&fs::read_to_string(&path)?)?;
Ok(Profile::new(config, Some(id.to_string())))
}
pub fn list_saved_profiles(&self) -> Result<Vec<String>> {
let root = self.runtime.profiles_root();
let mut out = Vec::new();
if root.exists() {
for entry in fs::read_dir(&root)? {
let entry = entry?;
if entry.path().join("profile.json").exists() {
if let Some(name) = entry.file_name().to_str() {
out.push(name.to_string());
}
}
}
}
out.sort();
Ok(out)
}
pub fn delete_profile(&self, id: &str) -> Result<()> {
let dir = self.runtime.profiles_root().join(id);
if dir.exists() {
fs::remove_dir_all(&dir)?;
}
Ok(())
}
fn profile_json_path(&self, id: &str) -> PathBuf {
self.runtime.profiles_root().join(id).join("profile.json")
}
pub async fn launch(
&self,
mut profile: Profile,
opts: LaunchOptions,
) -> Result<BrowserSession> {
self.runtime.install(false).await?;
if opts.randomize {
let id = profile.id.clone();
randomize_hardware(&mut profile.config, Some(&id));
randomize_platform_version(&mut profile.config);
}
self.browser.launch(profile, opts).await
}
#[cfg(feature = "control")]
pub async fn session(
&self,
profile: Profile,
mut opts: LaunchOptions,
) -> Result<Session> {
opts.cdp = true;
let engine = self.launch(profile, opts).await?;
if engine.cdp_url.is_none() {
let mut engine = engine;
let _ = engine.stop().await;
return Err(anyhow!(
"CDP endpoint unavailable — engine failed to expose remote-debugging port"
));
}
Session::connect(engine).await
}
pub async fn launch_cdp(
&self,
profile: Profile,
mut opts: LaunchOptions,
) -> Result<BrowserSession> {
opts.cdp = true;
let mut session = self.launch(profile, opts).await?;
if session.cdp_url.is_none() {
let _ = session.stop().await;
return Err(anyhow!(
"CDP endpoint unavailable — engine failed to expose remote-debugging port"
));
}
Ok(session)
}
pub async fn check_proxy(&self, proxy_url: &str) -> Result<ProxyCheckResult> {
let parsed = parse_proxy(proxy_url)?;
let udp_ms = if parsed.scheme == ProxyScheme::Socks5 {
probe_udp(&parsed, 6000).await.ok()
} else {
None
};
let geo = geo_check_via(Some(&parsed), "ip-api.com").await?;
let udp_ok = udp_ms.is_some();
Ok(ProxyCheckResult {
udp_ms,
geo,
would_enable_quic: udp_ok,
would_set_webrtc: if udp_ok {
WebRtcMode::Auto
} else {
WebRtcMode::TcpOnly
},
})
}
}