use anyhow::Result;
use super::{resolve_browser_name, resolve_cookie_source};
const THIN_COOKIE_RESULT_THRESHOLD: usize = 5;
pub async fn cmd_cookies(subcommand: &str, domain: &str, browser: &str) -> Result<()> {
match subcommand {
"export" => cmd_cookies_export(domain, browser),
_ => anyhow::bail!("Unknown cookies subcommand: {subcommand}. Use 'export'."),
}
}
fn cmd_cookies_export(domain: &str, browser: &str) -> Result<()> {
let browser_name = resolve_browser_name(browser)
.ok_or_else(|| anyhow::anyhow!("No browser specified. Use --cookies to select one."))?;
let source = resolve_cookie_source(&browser_name);
eprintln!("🍪 Exporting cookies for '{domain}' from {browser_name}");
let cookies = source.get_cookies(domain)?;
if cookies.is_empty() {
eprintln!("No cookies found for domain: {domain}");
return Ok(());
}
if let Some(msg) = bare_domain_thin_result_warning(domain, cookies.len()) {
eprintln!("{msg}");
}
println!("# Netscape HTTP Cookie File");
println!("# Exported by nab from {browser_name}");
println!("# Domain: {domain}");
println!();
for (name, value) in &cookies {
let include_subdomains = if domain.starts_with('.') {
"TRUE"
} else {
"FALSE"
};
let expiry = "0";
let secure = "FALSE";
let path = "/";
let cookie_domain = if domain.starts_with('.') {
domain.to_string()
} else {
format!(".{domain}")
};
println!(
"{cookie_domain}\t{include_subdomains}\t{path}\t{secure}\t{expiry}\t{name}\t{value}"
);
}
eprintln!("\n✅ Exported {} cookies", cookies.len());
Ok(())
}
fn bare_domain_thin_result_warning(domain: &str, cookie_count: usize) -> Option<String> {
let is_bare_domain =
!domain.starts_with('.') && !domain.starts_with("www.") && domain.split('.').count() == 2;
if is_bare_domain && cookie_count < THIN_COOKIE_RESULT_THRESHOLD {
Some(format!(
"⚠️ Only {cookie_count} cookies found for bare domain '{domain}'. \
Auth cookies often live on subdomains — try \
'nab cookies export www.{domain}' or another subdomain if results look thin."
))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warning_fires_on_bare_domain_with_few_cookies() {
let msg = bare_domain_thin_result_warning("linkedin.com", 3)
.expect("warning should fire for bare domain with thin results");
assert!(msg.contains("linkedin.com"));
assert!(msg.contains("www.linkedin.com"));
assert!(msg.contains('3'));
}
#[test]
fn warning_silent_on_bare_domain_with_enough_cookies() {
assert!(bare_domain_thin_result_warning("linkedin.com", 20).is_none());
}
#[test]
fn warning_silent_on_explicit_www_subdomain() {
assert!(bare_domain_thin_result_warning("www.linkedin.com", 1).is_none());
}
#[test]
fn warning_silent_on_subdomain() {
assert!(bare_domain_thin_result_warning("login.example.com", 1).is_none());
}
#[test]
fn warning_silent_on_dotted_leading_domain() {
assert!(bare_domain_thin_result_warning(".linkedin.com", 1).is_none());
}
}