862 lines
29 KiB
Rust
862 lines
29 KiB
Rust
mod color;
|
|
mod commands;
|
|
mod connection;
|
|
mod flags;
|
|
mod install;
|
|
mod output;
|
|
mod validation;
|
|
|
|
use serde_json::json;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::process::exit;
|
|
|
|
#[cfg(windows)]
|
|
use windows_sys::Win32::Foundation::CloseHandle;
|
|
#[cfg(windows)]
|
|
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
|
|
|
use commands::{gen_id, parse_command, ParseError};
|
|
use connection::{ensure_daemon, get_socket_dir, send_command};
|
|
use flags::{clean_args, parse_flags};
|
|
use install::run_install;
|
|
use output::{print_command_help, print_help, print_response, print_version};
|
|
|
|
fn parse_proxy(proxy_str: &str) -> serde_json::Value {
|
|
let Some(protocol_end) = proxy_str.find("://") else {
|
|
return json!({ "server": proxy_str });
|
|
};
|
|
let protocol = &proxy_str[..protocol_end + 3];
|
|
let rest = &proxy_str[protocol_end + 3..];
|
|
|
|
let Some(at_pos) = rest.rfind('@') else {
|
|
return json!({ "server": proxy_str });
|
|
};
|
|
|
|
let creds = &rest[..at_pos];
|
|
let server_part = &rest[at_pos + 1..];
|
|
let server = format!("{}{}", protocol, server_part);
|
|
|
|
let Some(colon_pos) = creds.find(':') else {
|
|
return json!({
|
|
"server": server,
|
|
"username": creds,
|
|
"password": ""
|
|
});
|
|
};
|
|
|
|
json!({
|
|
"server": server,
|
|
"username": &creds[..colon_pos],
|
|
"password": &creds[colon_pos + 1..]
|
|
})
|
|
}
|
|
|
|
fn run_session(args: &[String], session: &str, json_mode: bool) {
|
|
let subcommand = args.get(1).map(|s| s.as_str());
|
|
|
|
match subcommand {
|
|
Some("list") => {
|
|
let socket_dir = get_socket_dir();
|
|
let mut sessions: Vec<String> = Vec::new();
|
|
|
|
if let Ok(entries) = fs::read_dir(&socket_dir) {
|
|
for entry in entries.flatten() {
|
|
let name = entry.file_name().to_string_lossy().to_string();
|
|
// Look for pid files in socket directory
|
|
if name.ends_with(".pid") {
|
|
let session_name = name.strip_suffix(".pid").unwrap_or("");
|
|
if !session_name.is_empty() {
|
|
// Check if session is actually running
|
|
let pid_path = socket_dir.join(&name);
|
|
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
|
|
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
|
#[cfg(unix)]
|
|
let running = unsafe {
|
|
libc::kill(pid as i32, 0) == 0
|
|
|| std::io::Error::last_os_error().raw_os_error()
|
|
!= Some(libc::ESRCH)
|
|
};
|
|
#[cfg(windows)]
|
|
let running = unsafe {
|
|
let handle =
|
|
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
|
if handle != 0 {
|
|
CloseHandle(handle);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
if running {
|
|
sessions.push(session_name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if json_mode {
|
|
println!(
|
|
r#"{{"success":true,"data":{{"sessions":{}}}}}"#,
|
|
serde_json::to_string(&sessions).unwrap_or_default()
|
|
);
|
|
} else if sessions.is_empty() {
|
|
println!("No active sessions");
|
|
} else {
|
|
println!("Active sessions:");
|
|
for s in &sessions {
|
|
let marker = if s == session {
|
|
color::cyan("→")
|
|
} else {
|
|
" ".to_string()
|
|
};
|
|
println!("{} {}", marker, s);
|
|
}
|
|
}
|
|
}
|
|
None | Some(_) => {
|
|
// Just show current session
|
|
if json_mode {
|
|
println!(r#"{{"success":true,"data":{{"session":"{}"}}}}"#, session);
|
|
} else {
|
|
println!("{}", session);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
// Ignore SIGPIPE to prevent panic when piping to head/tail
|
|
#[cfg(unix)]
|
|
unsafe {
|
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
|
}
|
|
|
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
let flags = parse_flags(&args);
|
|
let clean = clean_args(&args);
|
|
|
|
let has_help = args.iter().any(|a| a == "--help" || a == "-h");
|
|
let has_version = args.iter().any(|a| a == "--version" || a == "-V");
|
|
|
|
if has_help {
|
|
if let Some(cmd) = clean.first() {
|
|
if print_command_help(cmd) {
|
|
return;
|
|
}
|
|
}
|
|
print_help();
|
|
return;
|
|
}
|
|
|
|
if has_version {
|
|
print_version();
|
|
return;
|
|
}
|
|
|
|
if let Some(ref risk_mode) = flags.risk_mode {
|
|
if !matches!(risk_mode.as_str(), "off" | "warn" | "block") {
|
|
let msg = format!(
|
|
"Invalid --risk-mode value: {} (expected off, warn, or block)",
|
|
risk_mode
|
|
);
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if args.iter().any(|a| a == "--profile") {
|
|
let msg =
|
|
"Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence.";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
if env::var("AGENT_BROWSER_PROFILE").is_ok() {
|
|
let msg =
|
|
"Project policy: AGENT_BROWSER_PROFILE is forbidden. Remove it and use --session-name.";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if args.iter().any(|a| a == "--channel") {
|
|
let msg = "Project policy: --channel is forbidden. Browser selection follows your existing browser session.";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
if env::var("AGENT_BROWSER_CHANNEL").is_ok() {
|
|
let msg =
|
|
"Project policy: AGENT_BROWSER_CHANNEL is forbidden. Remove it and use your existing browser session.";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if clean.is_empty() {
|
|
print_help();
|
|
return;
|
|
}
|
|
|
|
// Handle install separately
|
|
if clean.first().map(|s| s.as_str()) == Some("install") {
|
|
let with_deps = args.iter().any(|a| a == "--with-deps" || a == "-d");
|
|
run_install(with_deps);
|
|
return;
|
|
}
|
|
|
|
// Handle session separately (doesn't need daemon)
|
|
if clean.first().map(|s| s.as_str()) == Some("session") {
|
|
run_session(&clean, &flags.session, flags.json);
|
|
return;
|
|
}
|
|
|
|
let cmd = match parse_command(&clean, &flags) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
if flags.json {
|
|
let error_type = match &e {
|
|
ParseError::UnknownCommand { .. } => "unknown_command",
|
|
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
|
|
ParseError::MissingArguments { .. } => "missing_arguments",
|
|
ParseError::InvalidValue { .. } => "invalid_value",
|
|
ParseError::InvalidSessionName { .. } => "invalid_session_name",
|
|
};
|
|
println!(
|
|
r#"{{"success":false,"error":"{}","type":"{}"}}"#,
|
|
e.format().replace('\n', " "),
|
|
error_type
|
|
);
|
|
} else {
|
|
eprintln!("{}", color::red(&e.format()));
|
|
}
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
// Validate session name before starting daemon
|
|
if let Some(ref name) = flags.session_name {
|
|
if !validation::is_valid_session_name(name) {
|
|
let msg = validation::session_name_error(name);
|
|
if flags.json {
|
|
println!(
|
|
r#"{{"success":false,"error":"{}","type":"invalid_session_name"}}"#,
|
|
msg.replace('"', "\\\"")
|
|
);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
let daemon_result = match ensure_daemon(
|
|
&flags.session,
|
|
flags.headed,
|
|
flags.executable_path.as_deref(),
|
|
&flags.extensions,
|
|
flags.args.as_deref(),
|
|
flags.user_agent.as_deref(),
|
|
flags.proxy.as_deref(),
|
|
flags.proxy_bypass.as_deref(),
|
|
flags.ignore_https_errors,
|
|
flags.allow_file_access,
|
|
flags.state.as_deref(),
|
|
flags.provider.as_deref(),
|
|
flags.device.as_deref(),
|
|
flags.session_name.as_deref(),
|
|
flags.debug,
|
|
flags.download_path.as_deref(),
|
|
flags.tab_group.as_deref(),
|
|
flags.tab_group_plugin_id.as_deref(),
|
|
) {
|
|
Ok(result) => result,
|
|
Err(e) => {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), e);
|
|
}
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
// Warn if launch-time options were explicitly passed via CLI but daemon was already running
|
|
// Only warn about flags that were passed on the command line, not those set via environment
|
|
// variables (since the daemon already uses the env vars when it starts).
|
|
if daemon_result.already_running {
|
|
let ignored_flags: Vec<&str> = [
|
|
if flags.cli_executable_path {
|
|
Some("--executable-path")
|
|
} else {
|
|
None
|
|
},
|
|
if flags.cli_extensions {
|
|
Some("--extension")
|
|
} else {
|
|
None
|
|
},
|
|
if flags.cli_state {
|
|
Some("--state")
|
|
} else {
|
|
None
|
|
},
|
|
if flags.cli_args { Some("--args") } else { None },
|
|
if flags.cli_user_agent {
|
|
Some("--user-agent")
|
|
} else {
|
|
None
|
|
},
|
|
if flags.cli_proxy {
|
|
Some("--proxy")
|
|
} else {
|
|
None
|
|
},
|
|
if flags.cli_proxy_bypass {
|
|
Some("--proxy-bypass")
|
|
} else {
|
|
None
|
|
},
|
|
flags.ignore_https_errors.then_some("--ignore-https-errors"),
|
|
flags.cli_allow_file_access.then_some("--allow-file-access"),
|
|
flags.cli_download_path.then_some("--download-path"),
|
|
flags.cli_tab_group.then_some("--tab-group"),
|
|
flags
|
|
.cli_tab_group_plugin_id
|
|
.then_some("--tab-group-plugin-id"),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect();
|
|
|
|
if !ignored_flags.is_empty() && !flags.json {
|
|
eprintln!(
|
|
"{} {} ignored: daemon already running. Use 'agent-browser close' first to restart with new options.",
|
|
color::warning_indicator(),
|
|
ignored_flags.join(", ")
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate mutually exclusive options
|
|
if flags.cdp.is_some() && flags.provider.is_some() {
|
|
let msg = "Cannot use --cdp and -p/--provider together";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if flags.auto_connect && flags.cdp.is_some() {
|
|
let msg = "Cannot use --auto-connect and --cdp together";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if flags.auto_connect && flags.provider.is_some() {
|
|
let msg = "Cannot use --auto-connect and -p/--provider together";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if flags.provider.is_some() && !flags.extensions.is_empty() {
|
|
let msg = "Cannot use --extension with -p/--provider (extensions require local browser)";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
if flags.cdp.is_some() && !flags.extensions.is_empty() {
|
|
let msg = "Cannot use --extension with --cdp (extensions require local browser)";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
let mut attached_to_existing_browser = false;
|
|
|
|
// Auto-connect to existing browser
|
|
if flags.auto_connect {
|
|
let mut launch_cmd = json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"autoConnect": true
|
|
});
|
|
|
|
if flags.ignore_https_errors {
|
|
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
|
}
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
launch_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
|
|
if let Some(ref dp) = flags.download_path {
|
|
launch_cmd["downloadPath"] = json!(dp);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
launch_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
let err = match send_command(launch_cmd, &flags.session) {
|
|
Ok(resp) if resp.success => None,
|
|
Ok(resp) => Some(
|
|
resp.error
|
|
.unwrap_or_else(|| "Auto-connect failed".to_string()),
|
|
),
|
|
Err(e) => Some(e.to_string()),
|
|
};
|
|
|
|
if let Some(msg) = err {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
attached_to_existing_browser = true;
|
|
}
|
|
|
|
// Connect via CDP if --cdp flag is set
|
|
// Accepts either a port number (e.g., "9222") or a full URL (e.g., "ws://..." or "wss://...")
|
|
if let Some(ref cdp_value) = flags.cdp {
|
|
let mut launch_cmd = if cdp_value.starts_with("ws://")
|
|
|| cdp_value.starts_with("wss://")
|
|
|| cdp_value.starts_with("http://")
|
|
|| cdp_value.starts_with("https://")
|
|
{
|
|
// It's a URL - use cdpUrl field
|
|
json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"cdpUrl": cdp_value
|
|
})
|
|
} else {
|
|
// It's a port number - validate and use cdpPort field
|
|
let cdp_port: u16 = match cdp_value.parse::<u32>() {
|
|
Ok(0) => {
|
|
let msg = "Invalid CDP port: port must be greater than 0".to_string();
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
Ok(p) if p > 65535 => {
|
|
let msg = format!(
|
|
"Invalid CDP port: {} is out of range (valid range: 1-65535)",
|
|
p
|
|
);
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
Ok(p) => p as u16,
|
|
Err(_) => {
|
|
let msg = format!(
|
|
"Invalid CDP value: '{}' is not a valid port number or URL",
|
|
cdp_value
|
|
);
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
};
|
|
json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"cdpPort": cdp_port
|
|
})
|
|
};
|
|
|
|
if flags.ignore_https_errors {
|
|
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
|
}
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
launch_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
|
|
if let Some(ref dp) = flags.download_path {
|
|
launch_cmd["downloadPath"] = json!(dp);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
launch_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
let err = match send_command(launch_cmd, &flags.session) {
|
|
Ok(resp) if resp.success => None,
|
|
Ok(resp) => Some(
|
|
resp.error
|
|
.unwrap_or_else(|| "CDP connection failed".to_string()),
|
|
),
|
|
Err(e) => Some(e.to_string()),
|
|
};
|
|
|
|
if let Some(msg) = err {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
attached_to_existing_browser = true;
|
|
}
|
|
|
|
// Launch with cloud provider if -p flag is set
|
|
if let Some(ref provider) = flags.provider {
|
|
let mut launch_cmd = json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"provider": provider
|
|
});
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
launch_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
launch_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
match send_command(launch_cmd, &flags.session) {
|
|
Ok(resp) => {
|
|
if !resp.success {
|
|
let msg = resp
|
|
.error
|
|
.unwrap_or_else(|| "Provider connection failed".to_string());
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), e);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Project policy: when no explicit connection mode is provided,
|
|
// commands should attach to an existing browser.
|
|
// Try CDP :9333 first, then fall back to auto-connect discovery.
|
|
let can_try_default_cdp = flags.cdp.is_none()
|
|
&& !flags.auto_connect
|
|
&& flags.provider.is_none()
|
|
&& flags.executable_path.is_none()
|
|
&& flags.state.is_none()
|
|
&& flags.proxy.is_none()
|
|
&& flags.args.is_none()
|
|
&& flags.user_agent.is_none()
|
|
&& !flags.ignore_https_errors
|
|
&& !flags.allow_file_access
|
|
&& flags.extensions.is_empty();
|
|
|
|
if can_try_default_cdp {
|
|
let mut launch_cmd = json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"cdpPort": 9333
|
|
});
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
launch_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
launch_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
if let Ok(resp) = send_command(launch_cmd, &flags.session) {
|
|
attached_to_existing_browser = resp.success;
|
|
}
|
|
|
|
if !attached_to_existing_browser {
|
|
let mut auto_connect_cmd = json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"autoConnect": true
|
|
});
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
auto_connect_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
auto_connect_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
auto_connect_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
if let Ok(resp) = send_command(auto_connect_cmd, &flags.session) {
|
|
attached_to_existing_browser = resp.success;
|
|
}
|
|
}
|
|
}
|
|
if can_try_default_cdp && !attached_to_existing_browser {
|
|
let msg = "Project policy requires using your existing browser. Could not connect to CDP at localhost:9333 and auto-discovery also failed. Start Chrome with remote debugging (for example, --remote-debugging-port=9333), or pass --cdp <port|url>.";
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
// Launch headed browser or configure browser options (without CDP or provider)
|
|
if (flags.headed
|
|
|| flags.executable_path.is_some()
|
|
|| flags.state.is_some()
|
|
|| flags.proxy.is_some()
|
|
|| flags.args.is_some()
|
|
|| flags.user_agent.is_some()
|
|
|| flags.ignore_https_errors
|
|
|| flags.allow_file_access
|
|
|| flags.debug
|
|
|| flags.color_scheme.is_some()
|
|
|| flags.download_path.is_some())
|
|
&& flags.cdp.is_none()
|
|
&& flags.provider.is_none()
|
|
&& !attached_to_existing_browser
|
|
{
|
|
let mut launch_cmd = json!({
|
|
"id": gen_id(),
|
|
"action": "launch",
|
|
"headless": !flags.headed
|
|
});
|
|
|
|
let cmd_obj = launch_cmd
|
|
.as_object_mut()
|
|
.expect("json! macro guarantees object type");
|
|
|
|
// Add executable path if specified
|
|
if let Some(ref exec_path) = flags.executable_path {
|
|
cmd_obj.insert("executablePath".to_string(), json!(exec_path));
|
|
}
|
|
|
|
// Add state path if specified
|
|
if let Some(ref state_path) = flags.state {
|
|
cmd_obj.insert("storageState".to_string(), json!(state_path));
|
|
}
|
|
|
|
if let Some(ref proxy_str) = flags.proxy {
|
|
let mut proxy_obj = parse_proxy(proxy_str);
|
|
// Add bypass if specified
|
|
if let Some(ref bypass) = flags.proxy_bypass {
|
|
if let Some(obj) = proxy_obj.as_object_mut() {
|
|
obj.insert("bypass".to_string(), json!(bypass));
|
|
}
|
|
}
|
|
cmd_obj.insert("proxy".to_string(), proxy_obj);
|
|
}
|
|
|
|
if let Some(ref ua) = flags.user_agent {
|
|
cmd_obj.insert("userAgent".to_string(), json!(ua));
|
|
}
|
|
|
|
if let Some(ref a) = flags.args {
|
|
// Parse args (comma or newline separated)
|
|
let args_vec: Vec<String> = a
|
|
.split(&[',', '\n'][..])
|
|
.map(|s| s.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
cmd_obj.insert("args".to_string(), json!(args_vec));
|
|
}
|
|
|
|
if flags.ignore_https_errors {
|
|
launch_cmd["ignoreHTTPSErrors"] = json!(true);
|
|
}
|
|
|
|
if flags.allow_file_access {
|
|
launch_cmd["allowFileAccess"] = json!(true);
|
|
}
|
|
|
|
if let Some(ref cs) = flags.color_scheme {
|
|
launch_cmd["colorScheme"] = json!(cs);
|
|
}
|
|
|
|
if let Some(ref dp) = flags.download_path {
|
|
launch_cmd["downloadPath"] = json!(dp);
|
|
}
|
|
if let Some(ref tg) = flags.tab_group {
|
|
launch_cmd["tabGroup"] = json!(tg);
|
|
}
|
|
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
|
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
|
}
|
|
|
|
match send_command(launch_cmd, &flags.session) {
|
|
Ok(resp) => {
|
|
if !resp.success {
|
|
// Launch command failed (e.g., invalid state file)
|
|
let error_msg = resp
|
|
.error
|
|
.unwrap_or_else(|| "Browser launch failed".to_string());
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, error_msg);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), error_msg);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
} else {
|
|
eprintln!(
|
|
"{} Could not configure browser: {}",
|
|
color::error_indicator(),
|
|
e
|
|
);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
match send_command(cmd.clone(), &flags.session) {
|
|
Ok(resp) => {
|
|
let success = resp.success;
|
|
// Extract action for context-specific output handling
|
|
let action = cmd.get("action").and_then(|v| v.as_str());
|
|
print_response(&resp, flags.json, action);
|
|
if !success {
|
|
exit(1);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
if flags.json {
|
|
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
} else {
|
|
eprintln!("{} {}", color::error_indicator(), e);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_proxy_simple() {
|
|
let result = parse_proxy("http://proxy.com:8080");
|
|
assert_eq!(result["server"], "http://proxy.com:8080");
|
|
assert!(result.get("username").is_none());
|
|
assert!(result.get("password").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_with_auth() {
|
|
let result = parse_proxy("http://user:pass@proxy.com:8080");
|
|
assert_eq!(result["server"], "http://proxy.com:8080");
|
|
assert_eq!(result["username"], "user");
|
|
assert_eq!(result["password"], "pass");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_username_only() {
|
|
let result = parse_proxy("http://user@proxy.com:8080");
|
|
assert_eq!(result["server"], "http://proxy.com:8080");
|
|
assert_eq!(result["username"], "user");
|
|
assert_eq!(result["password"], "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_no_protocol() {
|
|
let result = parse_proxy("proxy.com:8080");
|
|
assert_eq!(result["server"], "proxy.com:8080");
|
|
assert!(result.get("username").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_socks5() {
|
|
let result = parse_proxy("socks5://proxy.com:1080");
|
|
assert_eq!(result["server"], "socks5://proxy.com:1080");
|
|
assert!(result.get("username").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_socks5_with_auth() {
|
|
let result = parse_proxy("socks5://admin:secret@proxy.com:1080");
|
|
assert_eq!(result["server"], "socks5://proxy.com:1080");
|
|
assert_eq!(result["username"], "admin");
|
|
assert_eq!(result["password"], "secret");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_proxy_complex_password() {
|
|
let result = parse_proxy("http://user:p@ss:w0rd@proxy.com:8080");
|
|
assert_eq!(result["server"], "http://proxy.com:8080");
|
|
assert_eq!(result["username"], "user");
|
|
assert_eq!(result["password"], "p@ss:w0rd");
|
|
}
|
|
}
|