Skip to content

Commit cd1f255

Browse files
authored
fix: handle proxy authentication via CDP Fetch.authRequired (#1000)
* fix: handle proxy authentication via CDP Fetch.authRequired Chrome's --proxy-server flag does not support credentials embedded in the URL. When a proxy requires authentication, Chrome receives a 407 from the proxy but has no way to respond with credentials, resulting in net::ERR_INVALID_AUTH_CREDENTIALS. Fix by: 1. Parsing credentials from the proxy URL (already done by parse_proxy) 2. Storing them in DaemonState.proxy_credentials 3. Enabling Fetch.enable with handleAuthRequests: true 4. Responding to Fetch.authRequired events with Fetch.continueWithAuth 5. Passing only the server URL (without credentials) to --proxy-server 6. Forwarding credentials to the daemon via dedicated env vars Also adds fallback to standard proxy env vars (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY) when AGENT_BROWSER_PROXY is not set. Fixes #990 * refactor: use typed struct for parse_proxy, fix double Fetch.enable and username-only auth - Replace serde_json::Value return from parse_proxy with a typed ParsedProxy struct - Fix double Fetch.enable call when both proxy auth and domain filter are active (the second call could overwrite handleAuthRequests from the first) - Allow username-only proxy auth (some proxies don't require a password) - Handle empty username/password in parse_proxy as None instead of Some("") - Use install_domain_filter_fetch in auto_launch for consistency - Update unit tests to use typed struct fields --------- Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
1 parent 23a117c commit cd1f255

7 files changed

Lines changed: 266 additions & 78 deletions

File tree

cli/src/connection.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ pub struct DaemonOptions<'a> {
188188
pub user_agent: Option<&'a str>,
189189
pub proxy: Option<&'a str>,
190190
pub proxy_bypass: Option<&'a str>,
191+
pub proxy_username: Option<&'a str>,
192+
pub proxy_password: Option<&'a str>,
191193
pub ignore_https_errors: bool,
192194
pub allow_file_access: bool,
193195
pub profile: Option<&'a str>,
@@ -233,6 +235,12 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) {
233235
if let Some(pb) = opts.proxy_bypass {
234236
cmd.env("AGENT_BROWSER_PROXY_BYPASS", pb);
235237
}
238+
if let Some(pu) = opts.proxy_username {
239+
cmd.env("AGENT_BROWSER_PROXY_USERNAME", pu);
240+
}
241+
if let Some(pp) = opts.proxy_password {
242+
cmd.env("AGENT_BROWSER_PROXY_PASSWORD", pp);
243+
}
236244
if opts.ignore_https_errors {
237245
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
238246
}

cli/src/flags.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,20 @@ pub fn parse_flags(args: &[String]) -> Flags {
353353
extensions,
354354
profile: env::var("AGENT_BROWSER_PROFILE").ok().or(config.profile),
355355
state: env::var("AGENT_BROWSER_STATE").ok().or(config.state),
356-
proxy: env::var("AGENT_BROWSER_PROXY").ok().or(config.proxy),
356+
proxy: env::var("AGENT_BROWSER_PROXY")
357+
.ok()
358+
.or(config.proxy)
359+
.or_else(|| env::var("HTTP_PROXY").ok())
360+
.or_else(|| env::var("http_proxy").ok())
361+
.or_else(|| env::var("HTTPS_PROXY").ok())
362+
.or_else(|| env::var("https_proxy").ok())
363+
.or_else(|| env::var("ALL_PROXY").ok())
364+
.or_else(|| env::var("all_proxy").ok()),
357365
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS")
358366
.ok()
359-
.or(config.proxy_bypass),
367+
.or(config.proxy_bypass)
368+
.or_else(|| env::var("NO_PROXY").ok())
369+
.or_else(|| env::var("no_proxy").ok()),
360370
args: env::var("AGENT_BROWSER_ARGS").ok().or(config.args),
361371
user_agent: env::var("AGENT_BROWSER_USER_AGENT")
362372
.ok()

cli/src/main.rs

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -54,34 +54,67 @@ fn print_json_error_with_type(message: impl AsRef<str>, error_type: &str) {
5454
}));
5555
}
5656

57-
fn parse_proxy(proxy_str: &str) -> serde_json::Value {
57+
struct ParsedProxy {
58+
server: String,
59+
username: Option<String>,
60+
password: Option<String>,
61+
}
62+
63+
fn parse_proxy(proxy_str: &str) -> ParsedProxy {
5864
let Some(protocol_end) = proxy_str.find("://") else {
59-
return json!({ "server": proxy_str });
65+
return ParsedProxy {
66+
server: proxy_str.to_string(),
67+
username: None,
68+
password: None,
69+
};
6070
};
6171
let protocol = &proxy_str[..protocol_end + 3];
6272
let rest = &proxy_str[protocol_end + 3..];
6373

6474
let Some(at_pos) = rest.rfind('@') else {
65-
return json!({ "server": proxy_str });
75+
return ParsedProxy {
76+
server: proxy_str.to_string(),
77+
username: None,
78+
password: None,
79+
};
6680
};
6781

6882
let creds = &rest[..at_pos];
6983
let server_part = &rest[at_pos + 1..];
7084
let server = format!("{}{}", protocol, server_part);
7185

72-
let Some(colon_pos) = creds.find(':') else {
73-
return json!({
74-
"server": server,
75-
"username": creds,
76-
"password": ""
77-
});
86+
let (username, password) = match creds.find(':') {
87+
Some(colon_pos) => {
88+
let u = &creds[..colon_pos];
89+
let p = &creds[colon_pos + 1..];
90+
(
91+
if u.is_empty() {
92+
None
93+
} else {
94+
Some(u.to_string())
95+
},
96+
if p.is_empty() {
97+
None
98+
} else {
99+
Some(p.to_string())
100+
},
101+
)
102+
}
103+
None => (
104+
if creds.is_empty() {
105+
None
106+
} else {
107+
Some(creds.to_string())
108+
},
109+
None,
110+
),
78111
};
79112

80-
json!({
81-
"server": server,
82-
"username": &creds[..colon_pos],
83-
"password": &creds[colon_pos + 1..]
84-
})
113+
ParsedProxy {
114+
server,
115+
username,
116+
password,
117+
}
85118
}
86119

87120
fn run_session(args: &[String], session: &str, json_mode: bool) {
@@ -330,15 +363,24 @@ fn main() {
330363
return;
331364
}
332365

366+
// Parse proxy URL to separate server from credentials for the daemon.
367+
let (proxy_server, proxy_username, proxy_password) = if let Some(ref proxy_str) = flags.proxy {
368+
let parsed = parse_proxy(proxy_str);
369+
(Some(parsed.server), parsed.username, parsed.password)
370+
} else {
371+
(None, None, None)
372+
};
333373
let daemon_opts = DaemonOptions {
334374
headed: flags.headed,
335375
debug: flags.debug,
336376
executable_path: flags.executable_path.as_deref(),
337377
extensions: &flags.extensions,
338378
args: flags.args.as_deref(),
339379
user_agent: flags.user_agent.as_deref(),
340-
proxy: flags.proxy.as_deref(),
380+
proxy: proxy_server.as_deref(),
341381
proxy_bypass: flags.proxy_bypass.as_deref(),
382+
proxy_username: proxy_username.as_deref(),
383+
proxy_password: proxy_password.as_deref(),
342384
ignore_https_errors: flags.ignore_https_errors,
343385
allow_file_access: flags.allow_file_access,
344386
profile: flags.profile.as_deref(),
@@ -694,12 +736,16 @@ fn main() {
694736
}
695737

696738
if let Some(ref proxy_str) = flags.proxy {
697-
let mut proxy_obj = parse_proxy(proxy_str);
698-
// Add bypass if specified
739+
let parsed = parse_proxy(proxy_str);
740+
let mut proxy_obj = json!({ "server": parsed.server });
741+
if let Some(ref username) = parsed.username {
742+
proxy_obj["username"] = json!(username);
743+
}
744+
if let Some(ref password) = parsed.password {
745+
proxy_obj["password"] = json!(password);
746+
}
699747
if let Some(ref bypass) = flags.proxy_bypass {
700-
if let Some(obj) = proxy_obj.as_object_mut() {
701-
obj.insert("bypass".to_string(), json!(bypass));
702-
}
748+
proxy_obj["bypass"] = json!(bypass);
703749
}
704750
cmd_obj.insert("proxy".to_string(), proxy_obj);
705751
}
@@ -1007,55 +1053,55 @@ mod tests {
10071053
#[test]
10081054
fn test_parse_proxy_simple() {
10091055
let result = parse_proxy("http://proxy.com:8080");
1010-
assert_eq!(result["server"], "http://proxy.com:8080");
1011-
assert!(result.get("username").is_none());
1012-
assert!(result.get("password").is_none());
1056+
assert_eq!(result.server, "http://proxy.com:8080");
1057+
assert!(result.username.is_none());
1058+
assert!(result.password.is_none());
10131059
}
10141060

10151061
#[test]
10161062
fn test_parse_proxy_with_auth() {
10171063
let result = parse_proxy("http://user:pass@proxy.com:8080");
1018-
assert_eq!(result["server"], "http://proxy.com:8080");
1019-
assert_eq!(result["username"], "user");
1020-
assert_eq!(result["password"], "pass");
1064+
assert_eq!(result.server, "http://proxy.com:8080");
1065+
assert_eq!(result.username.as_deref(), Some("user"));
1066+
assert_eq!(result.password.as_deref(), Some("pass"));
10211067
}
10221068

10231069
#[test]
10241070
fn test_parse_proxy_username_only() {
10251071
let result = parse_proxy("http://user@proxy.com:8080");
1026-
assert_eq!(result["server"], "http://proxy.com:8080");
1027-
assert_eq!(result["username"], "user");
1028-
assert_eq!(result["password"], "");
1072+
assert_eq!(result.server, "http://proxy.com:8080");
1073+
assert_eq!(result.username.as_deref(), Some("user"));
1074+
assert!(result.password.is_none());
10291075
}
10301076

10311077
#[test]
10321078
fn test_parse_proxy_no_protocol() {
10331079
let result = parse_proxy("proxy.com:8080");
1034-
assert_eq!(result["server"], "proxy.com:8080");
1035-
assert!(result.get("username").is_none());
1080+
assert_eq!(result.server, "proxy.com:8080");
1081+
assert!(result.username.is_none());
10361082
}
10371083

10381084
#[test]
10391085
fn test_parse_proxy_socks5() {
10401086
let result = parse_proxy("socks5://proxy.com:1080");
1041-
assert_eq!(result["server"], "socks5://proxy.com:1080");
1042-
assert!(result.get("username").is_none());
1087+
assert_eq!(result.server, "socks5://proxy.com:1080");
1088+
assert!(result.username.is_none());
10431089
}
10441090

10451091
#[test]
10461092
fn test_parse_proxy_socks5_with_auth() {
10471093
let result = parse_proxy("socks5://admin:secret@proxy.com:1080");
1048-
assert_eq!(result["server"], "socks5://proxy.com:1080");
1049-
assert_eq!(result["username"], "admin");
1050-
assert_eq!(result["password"], "secret");
1094+
assert_eq!(result.server, "socks5://proxy.com:1080");
1095+
assert_eq!(result.username.as_deref(), Some("admin"));
1096+
assert_eq!(result.password.as_deref(), Some("secret"));
10511097
}
10521098

10531099
#[test]
10541100
fn test_parse_proxy_complex_password() {
10551101
let result = parse_proxy("http://user:p@ss:w0rd@proxy.com:8080");
1056-
assert_eq!(result["server"], "http://proxy.com:8080");
1057-
assert_eq!(result["username"], "user");
1058-
assert_eq!(result["password"], "p@ss:w0rd");
1102+
assert_eq!(result.server, "http://proxy.com:8080");
1103+
assert_eq!(result.username.as_deref(), Some("user"));
1104+
assert_eq!(result.password.as_deref(), Some("p@ss:w0rd"));
10591105
}
10601106

10611107
#[test]

0 commit comments

Comments
 (0)