1024 lines
33 KiB
Rust
1024 lines
33 KiB
Rust
use super::*;
|
|
|
|
pub(crate) fn normalize_proxy_url(value: &str) -> Result<String, String> {
|
|
let value = value.trim();
|
|
let invalid = || {
|
|
"代理地址无效,请使用 http://host:port、https://host:port 或 socks5://host:port(不是 PAC 地址)".to_string()
|
|
};
|
|
if value.is_empty() || value.chars().any(|c| c.is_control() || c.is_whitespace()) {
|
|
return Err(invalid());
|
|
}
|
|
let value = if let Some(rest) = value.strip_prefix("socks://") {
|
|
format!("socks5://{rest}")
|
|
} else {
|
|
value.to_string()
|
|
};
|
|
let url = reqwest::Url::parse(&value).map_err(|_| invalid())?;
|
|
if !matches!(url.scheme(), "http" | "https" | "socks5" | "socks5h")
|
|
|| url.host_str().is_none()
|
|
|| url.port() == Some(0)
|
|
|| (url.port_or_known_default().is_none() && url.port().is_none())
|
|
|| !matches!(url.path(), "" | "/")
|
|
|| url.query().is_some()
|
|
|| url.fragment().is_some()
|
|
{
|
|
return Err(invalid());
|
|
}
|
|
Ok(value.trim_end_matches('/').to_string())
|
|
}
|
|
|
|
fn fixed_proxy(value: &str, scheme: &str) -> Result<String, String> {
|
|
parse_detected_proxy(&if value.contains("://") {
|
|
value.to_string()
|
|
} else {
|
|
format!("{scheme}://{value}")
|
|
})
|
|
}
|
|
|
|
fn parse_detected_proxy(value: &str) -> Result<String, String> {
|
|
let value = value.trim().trim_matches('\'').trim_matches('"');
|
|
if value.is_empty() {
|
|
return Err("系统代理地址为空".to_string());
|
|
}
|
|
if value.contains("://") {
|
|
normalize_proxy_url(value)
|
|
} else {
|
|
normalize_proxy_url(&format!("http://{value}"))
|
|
}
|
|
}
|
|
|
|
fn parse_env_proxy(name: &str, value: &str) -> Result<String, String> {
|
|
let value = value.trim().trim_matches('\'').trim_matches('"');
|
|
if value.is_empty() {
|
|
return Err("系统代理地址为空".to_string());
|
|
}
|
|
if !value.contains("://") && name.eq_ignore_ascii_case("socks_proxy") {
|
|
return normalize_proxy_url(&format!("socks5://{value}"));
|
|
}
|
|
parse_detected_proxy(value)
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", test))]
|
|
fn desktop_scalar(value: &str) -> String {
|
|
let value = value.trim().trim_matches('\'').trim_matches('"');
|
|
value
|
|
.strip_prefix("uint32 ")
|
|
.unwrap_or(value)
|
|
.trim()
|
|
.to_string()
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
fn parse_windows_proxy(value: &str) -> Result<String, String> {
|
|
if !value.contains('=') {
|
|
return fixed_proxy(value.trim(), "http");
|
|
}
|
|
for protocol in ["https", "http", "socks"] {
|
|
if let Some((_, address)) = value
|
|
.split(';')
|
|
.filter_map(|part| part.trim().split_once('='))
|
|
.find(|(key, _)| key.trim().eq_ignore_ascii_case(protocol))
|
|
{
|
|
return fixed_proxy(
|
|
address.trim(),
|
|
if protocol == "socks" {
|
|
"socks5"
|
|
} else {
|
|
"http"
|
|
},
|
|
);
|
|
}
|
|
}
|
|
Err("系统代理没有可用的 HTTP / HTTPS / SOCKS 地址".to_string())
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
fn pac_error() -> String {
|
|
"检测到 PAC / 自动发现代理,暂不支持转换为内核固定代理".to_string()
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn detect_system_proxy() -> Result<String, String> {
|
|
use windows_sys::Win32::{
|
|
Foundation::GlobalFree,
|
|
Networking::WinHttp::{
|
|
WinHttpGetIEProxyConfigForCurrentUser, WINHTTP_CURRENT_USER_IE_PROXY_CONFIG,
|
|
},
|
|
};
|
|
unsafe fn read_wide(ptr: *mut u16) -> String {
|
|
if ptr.is_null() {
|
|
return String::new();
|
|
}
|
|
let mut len = 0;
|
|
while *ptr.add(len) != 0 {
|
|
len += 1;
|
|
}
|
|
String::from_utf16_lossy(std::slice::from_raw_parts(ptr, len))
|
|
}
|
|
unsafe {
|
|
let mut config: WINHTTP_CURRENT_USER_IE_PROXY_CONFIG = std::mem::zeroed();
|
|
if WinHttpGetIEProxyConfigForCurrentUser(&mut config) == 0 {
|
|
return Err("无法读取当前用户的 Windows 系统代理".to_string());
|
|
}
|
|
let proxy = read_wide(config.lpszProxy);
|
|
let pac = read_wide(config.lpszAutoConfigUrl);
|
|
let auto_detect = config.fAutoDetect != 0;
|
|
for ptr in [
|
|
config.lpszProxy,
|
|
config.lpszProxyBypass,
|
|
config.lpszAutoConfigUrl,
|
|
] {
|
|
if !ptr.is_null() {
|
|
GlobalFree(ptr.cast());
|
|
}
|
|
}
|
|
select_windows_proxy(&proxy, &pac, auto_detect)
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
fn select_windows_proxy(proxy: &str, pac: &str, auto_detect: bool) -> Result<String, String> {
|
|
if !proxy.trim().is_empty() {
|
|
return parse_windows_proxy(proxy);
|
|
}
|
|
if !pac.trim().is_empty() || auto_detect {
|
|
return Err(pac_error());
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
|
fn env_proxy() -> Option<String> {
|
|
for name in [
|
|
"https_proxy",
|
|
"HTTPS_PROXY",
|
|
"all_proxy",
|
|
"ALL_PROXY",
|
|
"http_proxy",
|
|
"HTTP_PROXY",
|
|
"socks_proxy",
|
|
"SOCKS_PROXY",
|
|
] {
|
|
if let Ok(value) = std::env::var(name) {
|
|
if !value.trim().is_empty() {
|
|
if let Ok(url) = parse_env_proxy(name, &value) {
|
|
return Some(url);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
|
fn desktop_setting(program: &str, args: &[&str]) -> Result<String, String> {
|
|
let mut command = Command::new(program);
|
|
command
|
|
.args(args)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null());
|
|
let mut child = command
|
|
.spawn()
|
|
.map_err(|_| format!("无法运行 {program} 读取系统代理"))?;
|
|
let deadline = std::time::Instant::now() + Duration::from_millis(400);
|
|
loop {
|
|
match child.try_wait() {
|
|
Ok(Some(status)) => {
|
|
if !status.success() {
|
|
return Err(format!("{program} 读取系统代理失败"));
|
|
}
|
|
let output = child
|
|
.wait_with_output()
|
|
.map_err(|_| "读取系统代理失败".to_string())?;
|
|
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
|
}
|
|
Ok(None) if std::time::Instant::now() < deadline => {
|
|
std::thread::sleep(Duration::from_millis(20))
|
|
}
|
|
_ => {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
return Err("读取系统代理超时".to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn detect_system_proxy() -> Result<String, String> {
|
|
if let Ok(output) = desktop_setting("/usr/sbin/scutil", &["--proxy"]) {
|
|
if let Ok(url) = parse_macos_proxy(&output) {
|
|
if !url.is_empty() {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
}
|
|
if let Ok(url) = macos_networksetup_proxy() {
|
|
if !url.is_empty() {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
Ok(env_proxy().unwrap_or_default())
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn macos_networksetup_proxy() -> Result<String, String> {
|
|
let list = desktop_setting("/usr/sbin/networksetup", &["-listallnetworkservices"])?;
|
|
for service in list.lines().skip(1).filter_map(macos_network_service_name) {
|
|
for (flag, scheme) in [
|
|
("-getsecurewebproxy", "http"),
|
|
("-getwebproxy", "http"),
|
|
("-getsocksfirewallproxy", "socks5"),
|
|
] {
|
|
if let Ok(output) = desktop_setting("/usr/sbin/networksetup", &[flag, service]) {
|
|
if let Ok(url) = parse_networksetup_proxy(&output, scheme) {
|
|
if !url.is_empty() {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", test))]
|
|
fn parse_macos_proxy(output: &str) -> Result<String, String> {
|
|
let fields: std::collections::HashMap<_, _> = output
|
|
.lines()
|
|
.filter_map(|line| {
|
|
line.split_once(':')
|
|
.map(|(key, value)| (key.trim(), value.trim()))
|
|
})
|
|
.collect();
|
|
for (prefix, scheme) in [("HTTPS", "http"), ("HTTP", "http"), ("SOCKS", "socks5")] {
|
|
if fields.get(format!("{prefix}Enable").as_str()) != Some(&"1") {
|
|
continue;
|
|
}
|
|
let Some(host) = fields.get(format!("{prefix}Proxy").as_str()).copied() else {
|
|
continue;
|
|
};
|
|
let Some(port) = fields.get(format!("{prefix}Port").as_str()).copied() else {
|
|
continue;
|
|
};
|
|
if host.is_empty() || port.is_empty() || port == "0" {
|
|
continue;
|
|
}
|
|
if let Ok(url) = fixed_proxy(&format!("{}:{port}", bracket_host(host)), scheme) {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
if fields.get("ProxyAutoConfigEnable") == Some(&"1")
|
|
|| fields.get("ProxyAutoDiscoveryEnable") == Some(&"1")
|
|
{
|
|
return Ok(String::new());
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", test))]
|
|
fn macos_network_service_name(line: &str) -> Option<&str> {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('*') {
|
|
None
|
|
} else {
|
|
Some(line)
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", test))]
|
|
fn parse_networksetup_proxy(output: &str, scheme: &str) -> Result<String, String> {
|
|
let mut enabled = false;
|
|
let mut host = String::new();
|
|
let mut port = String::new();
|
|
for line in output.lines() {
|
|
let Some((key, value)) = line.split_once(':') else {
|
|
continue;
|
|
};
|
|
let key = key.trim();
|
|
let value = value.trim();
|
|
if key.eq_ignore_ascii_case("Enabled") {
|
|
enabled = value.eq_ignore_ascii_case("Yes") || value == "是";
|
|
} else if key.eq_ignore_ascii_case("Server") || key == "服务器" {
|
|
host = value.to_string();
|
|
} else if key.eq_ignore_ascii_case("Port") || key == "端口" {
|
|
port = value.to_string();
|
|
}
|
|
}
|
|
if !enabled || host.is_empty() || port.is_empty() || port == "0" {
|
|
return Ok(String::new());
|
|
}
|
|
fixed_proxy(&format!("{}:{port}", bracket_host(&host)), scheme)
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "linux", test))]
|
|
fn bracket_host(host: &str) -> String {
|
|
if host.contains(':') && !host.starts_with('[') {
|
|
format!("[{host}]")
|
|
} else {
|
|
host.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", test))]
|
|
fn gnome_manual_proxy(
|
|
mode: &str,
|
|
https_host: &str,
|
|
https_port: &str,
|
|
http_host: &str,
|
|
http_port: &str,
|
|
socks_host: &str,
|
|
socks_port: &str,
|
|
http_auth: &str,
|
|
) -> Result<String, String> {
|
|
match desktop_scalar(mode).as_str() {
|
|
"none" | "auto" => Ok(String::new()),
|
|
"manual" => {
|
|
let http_auth = desktop_scalar(http_auth).eq_ignore_ascii_case("true");
|
|
for (host, port, scheme, auth) in [
|
|
(https_host, https_port, "http", false),
|
|
(http_host, http_port, "http", http_auth),
|
|
(socks_host, socks_port, "socks5", false),
|
|
] {
|
|
let host = desktop_scalar(host);
|
|
let port = desktop_scalar(port);
|
|
if host.is_empty() || port.is_empty() || port == "0" || auth {
|
|
continue;
|
|
}
|
|
if let Ok(url) = fixed_proxy(&format!("{}:{port}", bracket_host(&host)), scheme) {
|
|
return Ok(url);
|
|
}
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
_ => Ok(String::new()),
|
|
}
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", test))]
|
|
fn parse_kde_proxy(proxy_type: &str, https: &str, http: &str, socks: &str) -> Result<Option<String>, String> {
|
|
match desktop_scalar(proxy_type).as_str() {
|
|
"0" | "" | "2" | "3" => Ok(Some(String::new())),
|
|
"4" => Ok(None),
|
|
_ => {
|
|
for value in [https, http, socks] {
|
|
let value = desktop_scalar(value);
|
|
if value.is_empty() {
|
|
continue;
|
|
}
|
|
if let Ok(url) = parse_detected_proxy(&value) {
|
|
return Ok(Some(url));
|
|
}
|
|
}
|
|
Ok(Some(String::new()))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn gnome_system_proxy() -> Result<Option<String>, String> {
|
|
let get = |schema: &str, key: &str| desktop_setting("gsettings", &["get", schema, key]);
|
|
let Ok(mode) = get("org.gnome.system.proxy", "mode") else {
|
|
return Ok(None);
|
|
};
|
|
let https_host = get("org.gnome.system.proxy.https", "host").unwrap_or_default();
|
|
let https_port = get("org.gnome.system.proxy.https", "port").unwrap_or_default();
|
|
let http_host = get("org.gnome.system.proxy.http", "host").unwrap_or_default();
|
|
let http_port = get("org.gnome.system.proxy.http", "port").unwrap_or_default();
|
|
let socks_host = get("org.gnome.system.proxy.socks", "host").unwrap_or_default();
|
|
let socks_port = get("org.gnome.system.proxy.socks", "port").unwrap_or_default();
|
|
let http_auth = get("org.gnome.system.proxy.http", "use-authentication").unwrap_or_default();
|
|
gnome_manual_proxy(
|
|
&mode,
|
|
&https_host,
|
|
&https_port,
|
|
&http_host,
|
|
&http_port,
|
|
&socks_host,
|
|
&socks_port,
|
|
&http_auth,
|
|
)
|
|
.map(Some)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn kde_config(key: &str) -> Result<String, String> {
|
|
for program in ["kreadconfig6", "kreadconfig5"] {
|
|
if let Ok(value) = desktop_setting(
|
|
program,
|
|
&[
|
|
"--file",
|
|
"kioslaverc",
|
|
"--group",
|
|
"Proxy Settings",
|
|
"--key",
|
|
key,
|
|
],
|
|
) {
|
|
return Ok(value);
|
|
}
|
|
}
|
|
Err("无法读取 KDE 系统代理".to_string())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn kde_system_proxy() -> Result<Option<String>, String> {
|
|
parse_kde_proxy(
|
|
&kde_config("ProxyType")?,
|
|
&kde_config("httpsProxy").unwrap_or_default(),
|
|
&kde_config("httpProxy").unwrap_or_default(),
|
|
&kde_config("socksProxy").unwrap_or_default(),
|
|
)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn linux_desktop() -> String {
|
|
std::env::var("XDG_CURRENT_DESKTOP")
|
|
.unwrap_or_default()
|
|
.to_ascii_lowercase()
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn detect_system_proxy() -> Result<String, String> {
|
|
if let Some(url) = env_proxy() {
|
|
return Ok(url);
|
|
}
|
|
let desktop = linux_desktop();
|
|
let gnome_desktop = ["gnome", "unity", "cinnamon", "budgie", "pantheon"]
|
|
.iter()
|
|
.any(|name| desktop.contains(name));
|
|
let kde_desktop = desktop.contains("kde") || desktop.contains("plasma");
|
|
if gnome_desktop {
|
|
match gnome_system_proxy() {
|
|
Ok(Some(url)) => return Ok(url),
|
|
Ok(None) => {}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
if kde_desktop || !gnome_desktop {
|
|
match kde_system_proxy() {
|
|
Ok(Some(url)) => return Ok(url),
|
|
Ok(None) | Err(_) => {}
|
|
}
|
|
}
|
|
if !gnome_desktop {
|
|
match gnome_system_proxy() {
|
|
Ok(Some(url)) => return Ok(url),
|
|
Ok(None) | Err(_) => {}
|
|
}
|
|
}
|
|
Ok(String::new())
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
|
fn detect_system_proxy() -> Result<String, String> {
|
|
Ok(String::new())
|
|
}
|
|
|
|
pub(crate) fn detect() -> String {
|
|
detect_system_proxy().unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn resolve(config: &GuiConfigFile) -> String {
|
|
if config.proxy_override {
|
|
config.proxy_url.clone()
|
|
} else {
|
|
detect()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn initialize_override(config: &mut GuiConfigFile, had_override: bool) -> bool {
|
|
if had_override {
|
|
return false;
|
|
}
|
|
// Before proxy-override existed, every non-empty proxy-url was explicitly
|
|
// entered by the user. Preserve that choice when migrating the config.
|
|
config.proxy_override = !config.proxy_url.trim().is_empty();
|
|
true
|
|
}
|
|
|
|
pub(crate) fn normalize_optional_proxy_url(value: &str) -> Result<String, String> {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
return Ok(String::new());
|
|
}
|
|
normalize_proxy_url(value)
|
|
}
|
|
|
|
fn apply(state: &GuiConfigState) -> Result<GuiConfigFile, String> {
|
|
let snapshot = state
|
|
.inner
|
|
.lock()
|
|
.map_err(|_| "代理配置锁已损坏".to_string())?
|
|
.clone();
|
|
let detected = if snapshot.proxy_override {
|
|
None
|
|
} else {
|
|
Some(detect())
|
|
};
|
|
let mut current = state
|
|
.inner
|
|
.lock()
|
|
.map_err(|_| "代理配置锁已损坏".to_string())?;
|
|
let mut next = current.clone();
|
|
next.proxy_url = if next.proxy_override {
|
|
next.proxy_url.clone()
|
|
} else {
|
|
detected.unwrap_or_else(detect)
|
|
};
|
|
commit(&mut current, next, patch_core_proxy_url, write_gui_config)?;
|
|
Ok(current.clone())
|
|
}
|
|
|
|
fn commit(
|
|
current: &mut GuiConfigFile,
|
|
next: GuiConfigFile,
|
|
mut patch: impl FnMut(&str) -> Result<(), String>,
|
|
persist: impl FnOnce(&GuiConfigFile) -> Result<(), String>,
|
|
) -> Result<(), String> {
|
|
let changed =
|
|
next.proxy_url != current.proxy_url || next.proxy_override != current.proxy_override;
|
|
patch(&next.proxy_url)?;
|
|
if changed {
|
|
if let Err(error) = persist(&next) {
|
|
return Err(config_update_error_with_rollback(
|
|
error,
|
|
patch(¤t.proxy_url).err(),
|
|
));
|
|
}
|
|
*current = next;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn refresh(state: &GuiConfigState) -> Result<(), String> {
|
|
apply(state).map(|_| ())
|
|
}
|
|
|
|
pub(crate) fn set_manual(state: &GuiConfigState, proxy_url: String) -> Result<(), String> {
|
|
let proxy_url = normalize_optional_proxy_url(&proxy_url)?;
|
|
let detected = if proxy_url.is_empty() {
|
|
Some(detect())
|
|
} else {
|
|
None
|
|
};
|
|
let mut current = state
|
|
.inner
|
|
.lock()
|
|
.map_err(|_| "代理配置锁已损坏".to_string())?;
|
|
let mut next = current.clone();
|
|
next.proxy_override = !proxy_url.is_empty();
|
|
next.proxy_url = if next.proxy_override {
|
|
proxy_url
|
|
} else {
|
|
detected.unwrap_or_else(detect)
|
|
};
|
|
commit(&mut current, next, patch_core_proxy_url, write_gui_config)
|
|
}
|
|
|
|
pub(crate) async fn verify_core_proxy(config: &GuiConfigFile) -> Result<(), String> {
|
|
let client = management_api::management_http_client()?;
|
|
let mut updated = false;
|
|
for attempt in 0..10 {
|
|
let response = client
|
|
.get(management_api::management_endpoint(config, "proxy-url")?)
|
|
.header(
|
|
"Authorization",
|
|
management_api::management_authorization(config)?,
|
|
)
|
|
.timeout(Duration::from_secs(2))
|
|
.send()
|
|
.await;
|
|
if let Ok(response) = response {
|
|
if response.status().is_success() {
|
|
if let Ok(value) = response.json::<serde_json::Value>().await {
|
|
if let Some(actual) = value.get("proxy-url").and_then(|v| v.as_str()) {
|
|
if actual == config.proxy_url {
|
|
return Ok(());
|
|
}
|
|
if attempt >= 2 && !updated {
|
|
let response = client
|
|
.put(management_api::management_endpoint(config, "proxy-url")?)
|
|
.header(
|
|
"Authorization",
|
|
management_api::management_authorization(config)?,
|
|
)
|
|
.json(&serde_json::json!({"value": config.proxy_url}))
|
|
.timeout(Duration::from_secs(3))
|
|
.send()
|
|
.await
|
|
.map_err(|_| "无法更新内核代理,请重启内核后重试".to_string())?;
|
|
if !response.status().is_success() {
|
|
return Err(
|
|
"内核拒绝代理更新,请重启内核或升级内核后重试".to_string()
|
|
);
|
|
}
|
|
updated = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
}
|
|
Err(
|
|
"代理配置已保存,但尚未确认内核已应用。请确认内核正在运行,必要时重启内核后重试。"
|
|
.to_string(),
|
|
)
|
|
}
|
|
|
|
static SYNC_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
|
|
|
async fn synchronize(app: &tauri::AppHandle, require_core: bool) -> Result<(), String> {
|
|
let _guard = SYNC_LOCK.lock().await;
|
|
let work_app = app.clone();
|
|
let config = tauri::async_runtime::spawn_blocking(move || {
|
|
apply(work_app.state::<GuiConfigState>().inner())
|
|
})
|
|
.await
|
|
.map_err(|e| e.to_string())??;
|
|
if require_core || app.state::<CoreProcessState>().managed_pid().is_some() {
|
|
verify_core_proxy(&config).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn prepare_oauth(app: &tauri::AppHandle) {
|
|
let _ = synchronize(app, true).await;
|
|
}
|
|
|
|
pub(crate) fn start_monitor(app: tauri::AppHandle) {
|
|
tauri::async_runtime::spawn(async move {
|
|
loop {
|
|
tokio::time::sleep(Duration::from_secs(15)).await;
|
|
if app
|
|
.state::<CoreProcessState>()
|
|
.shutting_down
|
|
.load(Ordering::Acquire)
|
|
{
|
|
return;
|
|
}
|
|
let _ = synchronize(&app, false).await;
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn failed_gui_write_rolls_back_core() {
|
|
let mut current = GuiConfigFile {
|
|
proxy_url: "http://localhost:1000".into(),
|
|
..GuiConfigFile::default()
|
|
};
|
|
let mut next = current.clone();
|
|
next.proxy_url = "http://localhost:2000".into();
|
|
let mut writes = Vec::new();
|
|
let result = commit(
|
|
&mut current,
|
|
next,
|
|
|url| {
|
|
writes.push(url.to_string());
|
|
Ok(())
|
|
},
|
|
|_| Err("disk full".into()),
|
|
);
|
|
assert!(result.is_err());
|
|
assert_eq!(writes, ["http://localhost:2000", "http://localhost:1000"]);
|
|
assert_eq!(current.proxy_url, "http://localhost:1000");
|
|
}
|
|
|
|
#[test]
|
|
fn failed_core_write_never_persists() {
|
|
let mut current = GuiConfigFile::default();
|
|
let mut next = current.clone();
|
|
next.proxy_url = "http://localhost:7890".into();
|
|
assert!(commit(
|
|
&mut current,
|
|
next,
|
|
|_| Err("read only".into()),
|
|
|_| panic!("must not persist")
|
|
)
|
|
.is_err());
|
|
assert!(current.proxy_url.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_refresh_does_not_rewrite_gui_config() {
|
|
let mut current = GuiConfigFile::default();
|
|
let next = current.clone();
|
|
let mut reconciled = false;
|
|
commit(
|
|
&mut current,
|
|
next,
|
|
|_| {
|
|
reconciled = true;
|
|
Ok(())
|
|
},
|
|
|_| panic!("no changes"),
|
|
)
|
|
.unwrap();
|
|
assert!(reconciled);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn configured_proxy_routes_upstream_but_bypasses_local_management() {
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
let proxy = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let proxy_url = format!("http://{}", proxy.local_addr().unwrap());
|
|
let proxy_request = tokio::spawn(async move {
|
|
let (mut socket, _) = proxy.accept().await.unwrap();
|
|
let mut bytes = [0; 4096];
|
|
let count = socket.read(&mut bytes).await.unwrap();
|
|
socket
|
|
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nvia")
|
|
.await
|
|
.unwrap();
|
|
String::from_utf8_lossy(&bytes[..count]).to_string()
|
|
});
|
|
let client = build_http_client_with_proxy(
|
|
reqwest::Client::builder().timeout(Duration::from_secs(2)),
|
|
&proxy_url,
|
|
"test",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
client
|
|
.get("http://proxy-target.invalid/test")
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap(),
|
|
"via"
|
|
);
|
|
assert!(proxy_request
|
|
.await
|
|
.unwrap()
|
|
.starts_with("GET http://proxy-target.invalid/test HTTP/1.1"));
|
|
let local = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let local_url = format!("http://{}", local.local_addr().unwrap());
|
|
let response = tokio::spawn(async move {
|
|
let (mut socket, _) = local.accept().await.unwrap();
|
|
let mut bytes = [0; 4096];
|
|
let count = socket.read(&mut bytes).await.unwrap();
|
|
assert!(count > 0);
|
|
socket
|
|
.write_all(
|
|
b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nlocal",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
assert_eq!(
|
|
client
|
|
.get(local_url)
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap(),
|
|
"local"
|
|
);
|
|
response.await.unwrap();
|
|
assert!(client
|
|
.get("http://proxy-target.invalid/test")
|
|
.send()
|
|
.await
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn windows_fixed_and_protocol_specific_proxies() {
|
|
assert_eq!(
|
|
parse_windows_proxy("127.0.0.1:7890").unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(
|
|
parse_windows_proxy("http=localhost:1;https=localhost:2;socks=localhost:3").unwrap(),
|
|
"http://localhost:2"
|
|
);
|
|
assert_eq!(
|
|
parse_windows_proxy("socks=[::1]:1080").unwrap(),
|
|
"socks5://[::1]:1080"
|
|
);
|
|
assert!(parse_windows_proxy("ftp=localhost:21").is_err());
|
|
assert_eq!(
|
|
select_windows_proxy("127.0.0.1:7890", "http://wpad/proxy.pac", true).unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert!(select_windows_proxy("", "http://wpad/proxy.pac", false).is_err());
|
|
assert_eq!(select_windows_proxy("", "", false).unwrap(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_pac_paths_invalid_ports_and_schemes() {
|
|
for url in [
|
|
"",
|
|
"localhost:7890",
|
|
"file:///proxy.pac",
|
|
"http://localhost/proxy.pac",
|
|
"http://localhost:0",
|
|
"http://localhost:99999",
|
|
"http://localhost:8?x=1",
|
|
"socks5://localhost",
|
|
"http://local host:8",
|
|
] {
|
|
assert!(normalize_proxy_url(url).is_err(), "{url}");
|
|
}
|
|
assert_eq!(
|
|
normalize_proxy_url(" http://localhost:7890/ ").unwrap(),
|
|
"http://localhost:7890"
|
|
);
|
|
for url in [
|
|
"http://localhost:7890",
|
|
"https://proxy.example",
|
|
"socks5://[::1]:1080",
|
|
"socks5h://user:pass@localhost:1080",
|
|
"socks://127.0.0.1:1080",
|
|
] {
|
|
assert!(normalize_proxy_url(url).is_ok(), "{url}");
|
|
}
|
|
assert_eq!(
|
|
normalize_proxy_url("socks://127.0.0.1:1080").unwrap(),
|
|
"socks5://127.0.0.1:1080"
|
|
);
|
|
assert!(normalize_optional_proxy_url("not a proxy").is_err());
|
|
assert!(normalize_optional_proxy_url("http://127.0.0.1:7890/path").is_err());
|
|
assert_eq!(
|
|
parse_detected_proxy(" '127.0.0.1:7890' ").unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert!(parse_detected_proxy("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn user_override_keeps_manual_proxy() {
|
|
let mut config = GuiConfigFile {
|
|
proxy_url: "socks5://127.0.0.1:1080".into(),
|
|
proxy_override: true,
|
|
..GuiConfigFile::default()
|
|
};
|
|
assert_eq!(resolve(&config), "socks5://127.0.0.1:1080");
|
|
config.proxy_override = false;
|
|
config.proxy_url = "http://localhost:1".into();
|
|
assert_eq!(resolve(&config), detect());
|
|
assert!(!initialize_override(&mut config, true));
|
|
let mut leftover = GuiConfigFile {
|
|
proxy_url: "http://127.0.0.1:7890".into(),
|
|
proxy_override: true,
|
|
..GuiConfigFile::default()
|
|
};
|
|
assert!(!initialize_override(&mut leftover, true));
|
|
assert!(leftover.proxy_override);
|
|
let mut legacy = GuiConfigFile {
|
|
proxy_url: "http://127.0.0.1:7890".into(),
|
|
..GuiConfigFile::default()
|
|
};
|
|
assert!(initialize_override(&mut legacy, false));
|
|
assert!(legacy.proxy_override);
|
|
assert_eq!(resolve(&legacy), "http://127.0.0.1:7890");
|
|
let mut auto = GuiConfigFile {
|
|
proxy_url: "http://127.0.0.1:7890".into(),
|
|
..GuiConfigFile::default()
|
|
};
|
|
assert!(!initialize_override(&mut auto, true));
|
|
assert!(!auto.proxy_override);
|
|
assert_eq!(resolve(&auto), detect());
|
|
assert_eq!(normalize_optional_proxy_url("").unwrap(), "");
|
|
assert_eq!(
|
|
normalize_optional_proxy_url(" socks5://127.0.0.1:7890/ ").unwrap(),
|
|
"socks5://127.0.0.1:7890"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn macos_fixed_and_pac() {
|
|
assert_eq!(
|
|
parse_macos_proxy(" HTTPEnable : 1\n HTTPProxy : 127.0.0.1\n HTTPPort : 7890")
|
|
.unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(parse_macos_proxy("HTTPEnable : 0").unwrap(), "");
|
|
assert_eq!(
|
|
parse_macos_proxy("ProxyAutoConfigEnable : 1\nHTTPEnable : 1\nHTTPProxy : 127.0.0.1\nHTTPPort : 7890")
|
|
.unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(parse_macos_proxy("ProxyAutoConfigEnable : 1").unwrap(), "");
|
|
assert_eq!(
|
|
parse_macos_proxy("HTTPSEnable : 1\nHTTPSProxy : ::1\nHTTPSPort : 7890").unwrap(),
|
|
"http://[::1]:7890"
|
|
);
|
|
assert_eq!(macos_network_service_name("*Wi-Fi"), None);
|
|
assert_eq!(macos_network_service_name("Wi-Fi"), Some("Wi-Fi"));
|
|
}
|
|
|
|
#[test]
|
|
fn macos_networksetup_and_unix_env_style_proxies() {
|
|
assert_eq!(
|
|
parse_networksetup_proxy(
|
|
"Enabled: Yes\nServer: 127.0.0.1\nPort: 7890",
|
|
"http",
|
|
)
|
|
.unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(
|
|
parse_networksetup_proxy("Enabled: No\nServer: 127.0.0.1\nPort: 7890", "http")
|
|
.unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(
|
|
parse_detected_proxy("127.0.0.1:7890").unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(
|
|
parse_detected_proxy("socks://127.0.0.1:1080").unwrap(),
|
|
"socks5://127.0.0.1:1080"
|
|
);
|
|
assert_eq!(
|
|
parse_env_proxy("socks_proxy", "127.0.0.1:1080").unwrap(),
|
|
"socks5://127.0.0.1:1080"
|
|
);
|
|
assert_eq!(
|
|
parse_env_proxy("https_proxy", "127.0.0.1:7890").unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(
|
|
gnome_manual_proxy(
|
|
"'manual'",
|
|
"'127.0.0.1'",
|
|
"uint32 7890",
|
|
"",
|
|
"uint32 0",
|
|
"",
|
|
"uint32 0",
|
|
"false",
|
|
)
|
|
.unwrap(),
|
|
"http://127.0.0.1:7890"
|
|
);
|
|
assert_eq!(
|
|
gnome_manual_proxy("none", "", "", "", "", "", "", "false").unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(
|
|
gnome_manual_proxy(
|
|
"manual",
|
|
"",
|
|
"0",
|
|
"127.0.0.1",
|
|
"7890",
|
|
"127.0.0.1",
|
|
"1080",
|
|
"true",
|
|
)
|
|
.unwrap(),
|
|
"socks5://127.0.0.1:1080"
|
|
);
|
|
assert_eq!(
|
|
gnome_manual_proxy("auto", "127.0.0.1", "7890", "", "", "", "", "false").unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(
|
|
parse_kde_proxy("1", "http://127.0.0.1:7890", "", "").unwrap(),
|
|
Some("http://127.0.0.1:7890".to_string())
|
|
);
|
|
assert_eq!(
|
|
parse_kde_proxy("0", "http://127.0.0.1:7890", "", "").unwrap(),
|
|
Some(String::new())
|
|
);
|
|
assert_eq!(parse_kde_proxy("2", "", "", "").unwrap(), Some(String::new()));
|
|
assert_eq!(parse_kde_proxy("4", "http://127.0.0.1:7890", "", "").unwrap(), None);
|
|
assert_eq!(
|
|
parse_kde_proxy("1", "http://localhost:0", "socks://127.0.0.1:1080", "").unwrap(),
|
|
Some("socks5://127.0.0.1:1080".to_string())
|
|
);
|
|
let mut current = GuiConfigFile {
|
|
proxy_url: "http://localhost:1000".into(),
|
|
..GuiConfigFile::default()
|
|
};
|
|
let mut next = current.clone();
|
|
next.proxy_override = true;
|
|
let mut persisted = false;
|
|
commit(
|
|
&mut current,
|
|
next,
|
|
|_| Ok(()),
|
|
|_| {
|
|
persisted = true;
|
|
Ok(())
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(persisted);
|
|
assert!(current.proxy_override);
|
|
}
|
|
}
|