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>
This commit is contained in:
Chris Tate
2026-03-24 12:52:39 -05:00
committed by GitHub
co-authored by ctate
parent 23a117c5c2
commit cd1f255129
7 changed files with 266 additions and 78 deletions
+85 -39
View File
@@ -54,34 +54,67 @@ fn print_json_error_with_type(message: impl AsRef<str>, error_type: &str) {
}));
}
fn parse_proxy(proxy_str: &str) -> serde_json::Value {
struct ParsedProxy {
server: String,
username: Option<String>,
password: Option<String>,
}
fn parse_proxy(proxy_str: &str) -> ParsedProxy {
let Some(protocol_end) = proxy_str.find("://") else {
return json!({ "server": proxy_str });
return ParsedProxy {
server: proxy_str.to_string(),
username: None,
password: None,
};
};
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 });
return ParsedProxy {
server: proxy_str.to_string(),
username: None,
password: None,
};
};
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": ""
});
let (username, password) = match creds.find(':') {
Some(colon_pos) => {
let u = &creds[..colon_pos];
let p = &creds[colon_pos + 1..];
(
if u.is_empty() {
None
} else {
Some(u.to_string())
},
if p.is_empty() {
None
} else {
Some(p.to_string())
},
)
}
None => (
if creds.is_empty() {
None
} else {
Some(creds.to_string())
},
None,
),
};
json!({
"server": server,
"username": &creds[..colon_pos],
"password": &creds[colon_pos + 1..]
})
ParsedProxy {
server,
username,
password,
}
}
fn run_session(args: &[String], session: &str, json_mode: bool) {
@@ -330,6 +363,13 @@ fn main() {
return;
}
// Parse proxy URL to separate server from credentials for the daemon.
let (proxy_server, proxy_username, proxy_password) = if let Some(ref proxy_str) = flags.proxy {
let parsed = parse_proxy(proxy_str);
(Some(parsed.server), parsed.username, parsed.password)
} else {
(None, None, None)
};
let daemon_opts = DaemonOptions {
headed: flags.headed,
debug: flags.debug,
@@ -337,8 +377,10 @@ fn main() {
extensions: &flags.extensions,
args: flags.args.as_deref(),
user_agent: flags.user_agent.as_deref(),
proxy: flags.proxy.as_deref(),
proxy: proxy_server.as_deref(),
proxy_bypass: flags.proxy_bypass.as_deref(),
proxy_username: proxy_username.as_deref(),
proxy_password: proxy_password.as_deref(),
ignore_https_errors: flags.ignore_https_errors,
allow_file_access: flags.allow_file_access,
profile: flags.profile.as_deref(),
@@ -694,12 +736,16 @@ fn main() {
}
if let Some(ref proxy_str) = flags.proxy {
let mut proxy_obj = parse_proxy(proxy_str);
// Add bypass if specified
let parsed = parse_proxy(proxy_str);
let mut proxy_obj = json!({ "server": parsed.server });
if let Some(ref username) = parsed.username {
proxy_obj["username"] = json!(username);
}
if let Some(ref password) = parsed.password {
proxy_obj["password"] = json!(password);
}
if let Some(ref bypass) = flags.proxy_bypass {
if let Some(obj) = proxy_obj.as_object_mut() {
obj.insert("bypass".to_string(), json!(bypass));
}
proxy_obj["bypass"] = json!(bypass);
}
cmd_obj.insert("proxy".to_string(), proxy_obj);
}
@@ -1007,55 +1053,55 @@ mod tests {
#[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());
assert_eq!(result.server, "http://proxy.com:8080");
assert!(result.username.is_none());
assert!(result.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");
assert_eq!(result.server, "http://proxy.com:8080");
assert_eq!(result.username.as_deref(), Some("user"));
assert_eq!(result.password.as_deref(), Some("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"], "");
assert_eq!(result.server, "http://proxy.com:8080");
assert_eq!(result.username.as_deref(), Some("user"));
assert!(result.password.is_none());
}
#[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());
assert_eq!(result.server, "proxy.com:8080");
assert!(result.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());
assert_eq!(result.server, "socks5://proxy.com:1080");
assert!(result.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");
assert_eq!(result.server, "socks5://proxy.com:1080");
assert_eq!(result.username.as_deref(), Some("admin"));
assert_eq!(result.password.as_deref(), Some("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");
assert_eq!(result.server, "http://proxy.com:8080");
assert_eq!(result.username.as_deref(), Some("user"));
assert_eq!(result.password.as_deref(), Some("p@ss:w0rd"));
}
#[test]