122 lines
5.4 KiB
Rust
122 lines
5.4 KiB
Rust
//! macOS SOCKS proxy ownership. Never clear a setting we no longer own.
|
|
use std::path::Path;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod mac {
|
|
use super::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::io::Write;
|
|
use std::process::Command;
|
|
|
|
const HOST: &str = "127.0.0.1";
|
|
const PORT: &str = "18743";
|
|
const RECORD: &str = "system-proxy.json";
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct Record { services: Vec<String> }
|
|
|
|
fn networksetup(args: &[&str]) -> Result<String, String> {
|
|
let result = Command::new("/usr/sbin/networksetup").args(args).output()
|
|
.map_err(|e| format!("无法读取系统网络设置:{e}"))?;
|
|
if !result.status.success() {
|
|
return Err(format!("无法更新系统代理:{}", String::from_utf8_lossy(&result.stderr).trim()));
|
|
}
|
|
Ok(String::from_utf8_lossy(&result.stdout).into_owned())
|
|
}
|
|
|
|
fn enabled(value: &str) -> bool {
|
|
value.lines().any(|line| line.trim().eq_ignore_ascii_case("Enabled: Yes") || line.trim().eq_ignore_ascii_case("Auto Proxy Discovery: On"))
|
|
}
|
|
|
|
fn services() -> Result<Vec<String>, String> {
|
|
let result = networksetup(&["-listallnetworkservices"])?;
|
|
Ok(result.lines().skip(1).filter(|s| !s.starts_with('*') && !s.trim().is_empty())
|
|
.map(str::to_owned).collect())
|
|
}
|
|
|
|
fn ours(service: &str) -> Result<bool, String> {
|
|
let value = networksetup(&["-getsocksfirewallproxy", service])?;
|
|
Ok(enabled(&value) && value.lines().any(|v| v.trim() == format!("Server: {HOST}"))
|
|
&& value.lines().any(|v| v.trim() == format!("Port: {PORT}")))
|
|
}
|
|
|
|
fn record_path(dir: &Path) -> std::path::PathBuf { dir.join(RECORD) }
|
|
|
|
pub fn active(dir: &Path) -> Result<bool, String> {
|
|
if !record_path(dir).exists() { return Ok(false); }
|
|
let record: Record = serde_json::from_slice(&std::fs::read(record_path(dir)).map_err(|e| e.to_string())?)
|
|
.map_err(|_| "系统代理恢复记录损坏,请检查网络设置".to_string())?;
|
|
for service in &record.services {
|
|
if ours(service)? { return Ok(true); }
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
pub fn restore(dir: &Path) -> Result<(), String> {
|
|
let path = record_path(dir);
|
|
if !path.exists() { return Ok(()); }
|
|
let record: Record = serde_json::from_slice(&std::fs::read(&path).map_err(|e| e.to_string())?)
|
|
.map_err(|_| "系统代理恢复记录损坏,请检查网络设置".to_string())?;
|
|
for service in &record.services {
|
|
if ours(service)? {
|
|
networksetup(&["-setsocksfirewallproxystate", service, "off"])?;
|
|
}
|
|
}
|
|
std::fs::remove_file(path).map_err(|e| format!("无法清理系统代理恢复记录:{e}"))
|
|
}
|
|
|
|
pub fn enable(dir: &Path) -> Result<(), String> {
|
|
// A stale record may mean a prior process crashed. Never overwrite it.
|
|
restore(dir)?;
|
|
let list = services()?;
|
|
if list.is_empty() { return Err("没有可用的 macOS 网络服务".into()); }
|
|
for service in &list {
|
|
for kind in ["-getwebproxy", "-getsecurewebproxy", "-getsocksfirewallproxy", "-getautoproxyurl", "-getproxyautodiscovery"] {
|
|
let current = networksetup(&[kind, service])?;
|
|
if enabled(¤t) || (kind == "-getsocksfirewallproxy"
|
|
&& current.lines().any(|line| line.starts_with("Server:")
|
|
&& line.trim() != "Server:" && line.trim() != format!("Server: {HOST}"))) {
|
|
return Err(format!("网络服务「{service}」已有代理设置,请先关闭其他代理再启用"));
|
|
}
|
|
}
|
|
}
|
|
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
|
// Record ownership before the first OS write, so interrupted startup can recover.
|
|
let path = record_path(dir);
|
|
let data = serde_json::to_vec(&Record { services: list.clone() }).map_err(|e| e.to_string())?;
|
|
let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path)
|
|
.map_err(|e| format!("无法保存系统代理恢复记录:{e}"))?;
|
|
#[cfg(unix)] {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())?;
|
|
}
|
|
file.write_all(&data).and_then(|_| file.sync_all()).map_err(|e| format!("无法持久化系统代理恢复记录:{e}"))?;
|
|
for service in &list {
|
|
if let Err(err) = networksetup(&["-setsocksfirewallproxy", service, HOST, PORT]) {
|
|
return match restore(dir) { Ok(()) => Err(err), Err(recovery) => Err(format!("{err};恢复失败:{recovery}")) };
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::enabled;
|
|
#[test]
|
|
fn parses_networksetup_state() {
|
|
assert!(enabled("Enabled: Yes\nServer: 127.0.0.1"));
|
|
assert!(!enabled("Enabled: No\nServer: 127.0.0.1"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub use mac::{active, enable, restore};
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub fn active(_: &Path) -> Result<bool, String> { Ok(false) }
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub fn restore(_: &Path) -> Result<(), String> { Ok(()) }
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub fn enable(_: &Path) -> Result<(), String> { Err("系统代理目前仅支持 macOS".into()) }
|