refactor: better proxy clipboard autofill

This commit is contained in:
zhom
2026-08-16 19:49:54 +04:00
parent a0175eab0d
commit 2be0d4df0b
7 changed files with 501 additions and 74 deletions
+106 -47
View File
@@ -1375,53 +1375,7 @@ impl ProxyManager {
}
}
// 4 parts: could be host:port:user:pass OR user:pass:host:port
4 => {
// Try to detect which format
let port_at_1 = parts[1].parse::<u16>().is_ok();
let port_at_3 = parts[3].parse::<u16>().is_ok();
match (port_at_1, port_at_3) {
// host:port:user:pass
(true, false) => {
let port = parts[1].parse::<u16>().unwrap();
ProxyParseResult::Parsed(ParsedProxyLine {
proxy_type: "http".to_string(),
host: parts[0].to_string(),
port,
username: Some(parts[2].to_string()),
password: Some(parts[3].to_string()),
vless_uri: None,
original_line: line.to_string(),
})
}
// user:pass:host:port
(false, true) => {
let port = parts[3].parse::<u16>().unwrap();
ProxyParseResult::Parsed(ParsedProxyLine {
proxy_type: "http".to_string(),
host: parts[2].to_string(),
port,
username: Some(parts[0].to_string()),
password: Some(parts[1].to_string()),
vless_uri: None,
original_line: line.to_string(),
})
}
// Both could be ports - ambiguous
(true, true) => ProxyParseResult::Ambiguous {
line: line.to_string(),
possible_formats: vec![
"host:port:username:password".to_string(),
"username:password:host:port".to_string(),
],
},
// Neither is a valid port
(false, false) => ProxyParseResult::Invalid {
line: line.to_string(),
reason: "No valid port number found".to_string(),
},
}
}
4 => Self::parse_colon_separated_quad(&parts, "http", line),
_ => ProxyParseResult::Invalid {
line: line.to_string(),
reason: format!("Unexpected format with {} parts", parts.len()),
@@ -1429,6 +1383,51 @@ impl ProxyManager {
}
}
// Resolve a four-part colon-separated body, which is either
// host:port:username:password or username:password:host:port. The port
// position tells the two apart; when both positions parse as a port the
// caller has to ask the user.
fn parse_colon_separated_quad(parts: &[&str], proxy_type: &str, line: &str) -> ProxyParseResult {
let port_at_1 = parts[1].parse::<u16>().ok();
let port_at_3 = parts[3].parse::<u16>().ok();
match (port_at_1, port_at_3) {
// host:port:user:pass
(Some(port), None) => ProxyParseResult::Parsed(ParsedProxyLine {
proxy_type: proxy_type.to_string(),
host: parts[0].to_string(),
port,
username: Some(parts[2].to_string()),
password: Some(parts[3].to_string()),
vless_uri: None,
original_line: line.to_string(),
}),
// user:pass:host:port
(None, Some(port)) => ProxyParseResult::Parsed(ParsedProxyLine {
proxy_type: proxy_type.to_string(),
host: parts[2].to_string(),
port,
username: Some(parts[0].to_string()),
password: Some(parts[1].to_string()),
vless_uri: None,
original_line: line.to_string(),
}),
// Both could be ports - ambiguous
(Some(_), Some(_)) => ProxyParseResult::Ambiguous {
line: line.to_string(),
possible_formats: vec![
"host:port:username:password".to_string(),
"username:password:host:port".to_string(),
],
},
// Neither is a valid port
(None, None) => ProxyParseResult::Invalid {
line: line.to_string(),
reason: "No valid port number found".to_string(),
},
}
}
// Try to parse URL format: protocol://username:password@host:port
fn try_parse_url_format(line: &str) -> Option<ProxyParseResult> {
if line.starts_with("vless://") {
@@ -1500,6 +1499,15 @@ impl ProxyManager {
}
}
} else {
// Vendors also hand out the colon-separated body behind a scheme, as in
// socks5://host:port:user:pass, so try that before plain host:port. An
// IPv6 literal splits into four too ("[", "", "1]", "8080" for [::1]:8080),
// so require every field to be populated before reading it that way.
let parts: Vec<&str> = rest.split(':').collect();
if parts.len() == 4 && parts.iter().all(|part| !part.is_empty()) {
return Some(Self::parse_colon_separated_quad(&parts, protocol, line));
}
// No auth, just host:port
if let Some(colon_pos) = rest.rfind(':') {
let host = &rest[..colon_pos];
@@ -3876,6 +3884,57 @@ mod tests {
_ => panic!("Expected Parsed"),
}
// Scheme in front of the colon-separated body
let results = ProxyManager::parse_txt_proxies("socks5://1.2.3.4:1080:admin:secret\n");
match &results[0] {
ProxyParseResult::Parsed(p) => {
assert_eq!(p.proxy_type, "socks5");
assert_eq!(p.host, "1.2.3.4");
assert_eq!(p.port, 1080);
assert_eq!(p.username.as_deref(), Some("admin"));
assert_eq!(p.password.as_deref(), Some("secret"));
}
_ => panic!("Expected Parsed"),
}
// Same, with the credentials in front
let results = ProxyManager::parse_txt_proxies("https://admin:secret:proxy.com:8443\n");
match &results[0] {
ProxyParseResult::Parsed(p) => {
assert_eq!(p.proxy_type, "https");
assert_eq!(p.host, "proxy.com");
assert_eq!(p.port, 8443);
assert_eq!(p.username.as_deref(), Some("admin"));
assert_eq!(p.password.as_deref(), Some("secret"));
}
_ => panic!("Expected Parsed"),
}
// An IPv6 literal splits into four parts as well, and must not be read as
// the colon-separated form
let results = ProxyManager::parse_txt_proxies("http://[::1]:8080\n");
match &results[0] {
ProxyParseResult::Parsed(p) => {
assert_eq!(p.host, "[::1]");
assert_eq!(p.port, 8080);
assert!(p.username.is_none());
}
_ => panic!("Expected Parsed"),
}
// A scheme-prefixed body that is ambiguous stays ambiguous
let results = ProxyManager::parse_txt_proxies("socks5://1234:5678:9012:3456\n");
match &results[0] {
ProxyParseResult::Ambiguous {
line,
possible_formats,
} => {
assert_eq!(line, "socks5://1234:5678:9012:3456");
assert_eq!(possible_formats.len(), 2);
}
_ => panic!("Expected Ambiguous"),
}
// Ambiguous: both positions could be ports
let results = ProxyManager::parse_txt_proxies("1234:5678:9012:3456\n");
match &results[0] {