Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ mod tests {
executable_path: None,
extensions: Vec::new(),
cdp: None,
proxy: None,
}
}

Expand Down
11 changes: 9 additions & 2 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,23 @@ pub fn ensure_daemon(
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
let exe_dir = exe_path.parent().unwrap();

let daemon_paths = [
let mut daemon_paths = vec![
exe_dir.join("daemon.js"),
exe_dir.join("../dist/daemon.js"),
PathBuf::from("dist/daemon.js"),
];

// Check AGENT_BROWSER_HOME environment variable
if let Ok(home) = env::var("AGENT_BROWSER_HOME") {
let home_path = PathBuf::from(&home);
daemon_paths.insert(0, home_path.join("dist/daemon.js"));
daemon_paths.insert(1, home_path.join("daemon.js"));
}

let daemon_path = daemon_paths
.iter()
.find(|p| p.exists())
.ok_or("Daemon not found. Run from project directory or ensure daemon.js is alongside binary.")?;
.ok_or("Daemon not found. Set AGENT_BROWSER_HOME environment variable or run from project directory.")?;

// Spawn daemon as a fully detached background process
#[cfg(unix)]
Expand Down
10 changes: 9 additions & 1 deletion cli/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Flags {
pub executable_path: Option<String>,
pub cdp: Option<String>,
pub extensions: Vec<String>,
pub proxy: Option<String>,
}

pub fn parse_flags(args: &[String]) -> Flags {
Expand All @@ -28,6 +29,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
cdp: None,
extensions: extensions_env,
proxy: None,
};

let mut i = 0;
Expand Down Expand Up @@ -67,6 +69,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--proxy" => {
if let Some(p) = args.get(i + 1) {
flags.proxy = Some(p.clone());
i += 1;
}
}
_ => {}
}
i += 1;
Expand All @@ -81,7 +89,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
// Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension"];
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy"];

for arg in args.iter() {
if skip_next {
Expand Down
106 changes: 101 additions & 5 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,36 @@ use flags::{clean_args, parse_flags};
use install::run_install;
use output::{print_command_help, print_help, print_response};

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());

Expand Down Expand Up @@ -228,17 +258,24 @@ fn main() {
}
}

// Launch headed browser if --headed flag is set (without CDP)
if flags.headed && flags.cdp.is_none() {
let launch_cmd = json!({
// Launch headed browser or proxy if flags are set (without CDP)
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() {
let mut launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"headless": false
"headless": !flags.headed
});

if let Some(ref proxy_str) = flags.proxy {
let proxy_obj = parse_proxy(proxy_str);
launch_cmd.as_object_mut()
.expect("json! macro guarantees object type")
.insert("proxy".to_string(), proxy_obj);
}

if let Err(e) = send_command(launch_cmd, &flags.session) {
if !flags.json {
eprintln!("\x1b[33m⚠\x1b[0m Could not launch headed browser: {}", e);
eprintln!("\x1b[33m⚠\x1b[0m Could not configure browser: {}", e);
}
}
}
Expand All @@ -261,3 +298,62 @@ fn main() {
}
}
}

#[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");
}
}
1 change: 1 addition & 0 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,7 @@ Options:
--headers <json> HTTP headers scoped to URL's origin (for auth)
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
--extension <path> Load browser extensions (repeatable).
--proxy <url> Proxy server (http://[user:pass@]host:port)
--json JSON output
--full, -f Full page screenshot
--headed Show browser window (not headless)
Expand Down
11 changes: 8 additions & 3 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ export class BrowserManager {
args: [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`],
viewport,
extraHTTPHeaders: options.headers,
...(options.proxy && { proxy: options.proxy }),
}
);
this.isPersistentContext = true;
Expand All @@ -691,10 +692,14 @@ export class BrowserManager {
executablePath: options.executablePath,
});
this.cdpPort = null;
context = await this.browser.newContext({ viewport, extraHTTPHeaders: options.headers });
context = await this.browser.newContext({
viewport,
extraHTTPHeaders: options.headers,
...(options.proxy && { proxy: options.proxy }),
});
}

context.setDefaultTimeout(10000);
context.setDefaultTimeout(60000);
this.contexts.push(context);

const page = context.pages()[0] ?? (await context.newPage());
Expand Down Expand Up @@ -828,7 +833,7 @@ export class BrowserManager {
const context = await this.browser.newContext({
viewport: viewport ?? { width: 1280, height: 720 },
});
context.setDefaultTimeout(10000);
context.setDefaultTimeout(60000);
this.contexts.push(context);

const page = await context.newPage();
Expand Down
11 changes: 11 additions & 0 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ const launchSchema = baseCommandSchema.extend({
.optional(),
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
cdpPort: z.number().positive().optional(),
executablePath: z.string().optional(),
extensions: z.array(z.string()).optional(),
headers: z.record(z.string()).optional(),
proxy: z
.object({
server: z.string().min(1),
bypass: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
})
.optional(),
});

const navigateSchema = baseCommandSchema.extend({
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export interface LaunchCommand extends BaseCommand {
executablePath?: string;
cdpPort?: number;
extensions?: string[];
proxy?: {
server: string;
bypass?: string;
username?: string;
password?: string;
};
}

export interface NavigateCommand extends BaseCommand {
Expand Down