cli: make --stealth a global flag, available on every subcommand

--stealth was a per-subcommand flag defined separately on fetch, serve,
and mcp, and missing from scrape entirely. You could not enable stealth
for parallel scraping, and `obscura --stealth fetch` (flag before the
subcommand) errored.

Promote --stealth to a single global flag like --proxy, so it is valid
before or after any subcommand and applies uniformly to fetch, serve,
scrape, and mcp. scrape now forwards it to its workers via OBSCURA_STEALTH
(mirroring OBSCURA_PROXY), and the bare `obscura` default CDP server
honors it too.

Verified: navigating directly to a blocklisted tracker renders 55503
chars without stealth and 39 (blocked) with it, identically on fetch and
scrape. Obstacle course 33/33.
This commit is contained in:
SGavrl committed 2026-06-24 09:46:10 +00:00
1 parent d2206fea84
commit 8c14c4393d
2 files changed
+18 -16

No files matched your search

+14 -15
View File
@@ -26,6 +26,12 @@ struct Args {
#[arg(long, global = true)] #[arg(long, global = true)]
proxy: Option<String>, proxy: Option<String>,
/// Enable stealth mode (consistent browser fingerprint, and with the
/// `stealth` build feature, TLS impersonation plus tracker blocking).
/// Global: applies to fetch, serve, scrape, and mcp.
#[arg(long, global = true)]
stealth: bool,
#[arg(long)] #[arg(long)]
obey_robots: bool, obey_robots: bool,
@@ -69,9 +75,6 @@ enum Command {
#[arg(long)] #[arg(long)]
user_agent: Option<String>, user_agent: Option<String>,
#[arg(long)]
stealth: bool,
#[arg(long, default_value_t = 1)] #[arg(long, default_value_t = 1)]
workers: u16, workers: u16,
@@ -116,9 +119,6 @@ enum Command {
#[arg(long)] #[arg(long)]
user_agent: Option<String>, user_agent: Option<String>,
#[arg(long)]
stealth: bool,
#[arg(long, short)] #[arg(long, short)]
eval: Option<String>, eval: Option<String>,
@@ -166,9 +166,6 @@ enum Command {
#[arg(long)] #[arg(long)]
user_agent: Option<String>, user_agent: Option<String>,
#[arg(long)]
stealth: bool,
}, },
} }
@@ -313,9 +310,10 @@ async fn main() -> anyhow::Result<()> {
} }
let global_proxy = args.proxy.clone(); let global_proxy = args.proxy.clone();
let stealth = args.stealth;
match args.command { match args.command {
Some(Command::Serve { port, host, proxy, user_agent, stealth, workers, allow_file_access, storage_dir, quiet: _ }) => { Some(Command::Serve { port, host, proxy, user_agent, workers, allow_file_access, storage_dir, quiet: _ }) => {
let proxy = merge_proxy(global_proxy.clone(), proxy); let proxy = merge_proxy(global_proxy.clone(), proxy);
print_banner(port); print_banner(port);
if let Some(ref dir) = storage_dir { if let Some(ref dir) = storage_dir {
@@ -346,13 +344,13 @@ async fn main() -> anyhow::Result<()> {
).await?; ).await?;
} }
} }
Some(Command::Fetch { url, dump, selector, wait, timeout, wait_until, user_agent, stealth, eval, output, quiet, storage_dir }) => { Some(Command::Fetch { url, dump, selector, wait, timeout, wait_until, user_agent, eval, output, quiet, storage_dir }) => {
run_fetch(&url, dump, selector, wait, timeout, &wait_until, user_agent, stealth, eval, output, quiet, global_proxy, storage_dir, args.allow_private_network).await?; run_fetch(&url, dump, selector, wait, timeout, &wait_until, user_agent, stealth, eval, output, quiet, global_proxy, storage_dir, args.allow_private_network).await?;
} }
Some(Command::Scrape { urls, eval, concurrency, format, timeout, quiet }) => { Some(Command::Scrape { urls, eval, concurrency, format, timeout, quiet }) => {
run_parallel_scrape(urls, eval, concurrency.get(), &format, timeout, quiet, global_proxy).await?; run_parallel_scrape(urls, eval, concurrency.get(), &format, timeout, quiet, global_proxy, stealth).await?;
} }
Some(Command::Mcp { http, host, port, proxy, user_agent, stealth }) => { Some(Command::Mcp { http, host, port, proxy, user_agent }) => {
let mcp_proxy = merge_proxy(global_proxy.clone(), proxy); let mcp_proxy = merge_proxy(global_proxy.clone(), proxy);
if http { if http {
obscura_mcp::http::run(host, port, mcp_proxy, user_agent, stealth).await?; obscura_mcp::http::run(host, port, mcp_proxy, user_agent, stealth).await?;
@@ -365,7 +363,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref proxy) = args.proxy { if let Some(ref proxy) = args.proxy {
tracing::info!("Using proxy: {}", proxy); tracing::info!("Using proxy: {}", proxy);
} }
obscura_cdp::start_with_options(args.port, args.proxy, false).await?; obscura_cdp::start_with_options(args.port, args.proxy, stealth).await?;
} }
} }
@@ -825,6 +823,7 @@ async fn run_parallel_scrape(
timeout_secs: u64, timeout_secs: u64,
quiet: bool, quiet: bool,
proxy: Option<String>, proxy: Option<String>,
stealth: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let total = urls.len(); let total = urls.len();
let start = Instant::now(); let start = Instant::now();
@@ -877,6 +876,7 @@ async fn run_parallel_scrape(
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null()) .stderr(std::process::Stdio::null())
.env("OBSCURA_PROXY", proxy.as_deref().unwrap_or("")) .env("OBSCURA_PROXY", proxy.as_deref().unwrap_or(""))
.env("OBSCURA_STEALTH", if stealth { "1" } else { "" })
.spawn() .spawn()
{ {
Ok(c) => c, Ok(c) => c,
@@ -1498,7 +1498,6 @@ mod tests {
timeout: 30, timeout: 30,
wait_until: "load".to_string(), wait_until: "load".to_string(),
user_agent: None, user_agent: None,
stealth: false,
eval: None, eval: None,
quiet: true, quiet: true,
output: None, output: None,
+4 -1
View File
@@ -51,7 +51,10 @@ async fn main() {
.ok() .ok()
.map(|value| value.trim().to_string()) .map(|value| value.trim().to_string())
.filter(|value| !value.is_empty()); .filter(|value| !value.is_empty());
let context = Arc::new(BrowserContext::with_options("worker".to_string(), proxy, false)); let stealth = std::env::var("OBSCURA_STEALTH")
.map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
.unwrap_or(false);
let context = Arc::new(BrowserContext::with_options("worker".to_string(), proxy, stealth));
let mut page = Page::new("page-1".to_string(), context); let mut page = Page::new("page-1".to_string(), context);
let stdin = tokio::io::stdin(); let stdin = tokio::io::stdin();