341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
use rusqlite::{Connection, OpenFlags};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::path::PathBuf;
|
||
|
||
#[derive(Debug, Serialize, Clone)]
|
||
pub struct V2raynNode {
|
||
pub index_id: String,
|
||
pub config_type: i64,
|
||
pub remarks: String,
|
||
pub address: String,
|
||
pub port: i64,
|
||
pub id: Option<String>,
|
||
pub security: Option<String>,
|
||
pub network: Option<String>,
|
||
pub stream_security: Option<String>,
|
||
pub flow: Option<String>,
|
||
pub sni: Option<String>,
|
||
pub fingerprint: Option<String>,
|
||
pub public_key: Option<String>,
|
||
pub short_id: Option<String>,
|
||
pub spider_x: Option<String>,
|
||
pub request_host: Option<String>,
|
||
pub path: Option<String>,
|
||
pub alter_id: Option<i64>,
|
||
pub alter_id_i: Option<i64>,
|
||
}
|
||
|
||
/// 自动探测 v2rayN DB 路径
|
||
fn find_v2rayn_db() -> Option<PathBuf> {
|
||
// 1. %APPDATA%\v2rayN
|
||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||
let p = PathBuf::from(&appdata)
|
||
.join("v2rayN")
|
||
.join("guiConfigs")
|
||
.join("guiNDB.db");
|
||
if p.exists() {
|
||
return Some(p);
|
||
}
|
||
}
|
||
|
||
// 2. Scoop 安装:扫描常见盘符 + 用户目录下的 Scoop
|
||
let scoop_roots: Vec<PathBuf> = {
|
||
let mut roots = vec![];
|
||
// 用户目录下的 Scoop(默认安装位置)
|
||
if let Ok(home) = std::env::var("USERPROFILE") {
|
||
roots.push(PathBuf::from(&home).join("scoop"));
|
||
roots.push(PathBuf::from(&home).join("Scoop"));
|
||
}
|
||
// 各盘符根目录
|
||
for drive in ["C", "D", "E", "F", "G"] {
|
||
roots.push(PathBuf::from(format!("{drive}:\\scoop")));
|
||
roots.push(PathBuf::from(format!("{drive}:\\Scoop")));
|
||
}
|
||
roots
|
||
};
|
||
for root in &scoop_roots {
|
||
let p = root
|
||
.join("persist")
|
||
.join("v2rayn")
|
||
.join("guiConfigs")
|
||
.join("guiNDB.db");
|
||
if p.exists() {
|
||
return Some(p);
|
||
}
|
||
// 有些 scoop bucket 用 v2rayn 全小写或带版本
|
||
let p2 = root
|
||
.join("persist")
|
||
.join("v2rayN")
|
||
.join("guiConfigs")
|
||
.join("guiNDB.db");
|
||
if p2.exists() {
|
||
return Some(p2);
|
||
}
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
/// 将 DB 文件复制到临时目录,绕过文件锁
|
||
fn copy_to_temp(src: &PathBuf) -> Result<PathBuf, String> {
|
||
let tmp = std::env::temp_dir().join("dev_manager_v2rayn_copy.db");
|
||
std::fs::copy(src, &tmp).map_err(|e| format!("复制 DB 失败: {e}"))?;
|
||
Ok(tmp)
|
||
}
|
||
|
||
/// 扫描 v2rayN 节点列表。db_path 为空时自动探测。
|
||
#[tauri::command]
|
||
pub fn scan_v2rayn_nodes(db_path: Option<String>) -> Result<Vec<V2raynNode>, String> {
|
||
let path = match db_path {
|
||
Some(p) => PathBuf::from(p),
|
||
None => {
|
||
find_v2rayn_db().ok_or_else(|| "未找到 v2rayN 配置文件,请手动指定路径".to_string())?
|
||
}
|
||
};
|
||
|
||
// 复制避免锁冲突
|
||
let tmp = copy_to_temp(&path)?;
|
||
|
||
let conn = Connection::open_with_flags(&tmp, OpenFlags::SQLITE_OPEN_READ_ONLY)
|
||
.map_err(|e| format!("打开 DB 失败: {e}"))?;
|
||
|
||
let mut stmt = conn
|
||
.prepare(
|
||
"SELECT IndexId, ConfigType, Remarks, Address, Port, Id, Security, Network,
|
||
StreamSecurity, Flow, Sni, Fingerprint, PublicKey, ShortId, SpiderX,
|
||
RequestHost, Path, AlterId
|
||
FROM ProfileItem
|
||
WHERE ConfigType IN (1, 3, 5, 6, 7)
|
||
ORDER BY Remarks",
|
||
)
|
||
.map_err(|e| format!("查询失败: {e}"))?;
|
||
|
||
let nodes = stmt
|
||
.query_map([], |row| {
|
||
Ok(V2raynNode {
|
||
index_id: row.get(0)?,
|
||
config_type: row.get(1)?,
|
||
remarks: row.get::<_, Option<String>>(2)?.unwrap_or_default(),
|
||
address: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||
port: row.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||
id: row.get(5)?,
|
||
security: row.get(6)?,
|
||
network: row.get(7)?,
|
||
stream_security: row.get(8)?,
|
||
flow: row.get(9)?,
|
||
sni: row.get(10)?,
|
||
fingerprint: row.get(11)?,
|
||
public_key: row.get(12)?,
|
||
short_id: row.get(13)?,
|
||
spider_x: row.get(14)?,
|
||
request_host: row.get(15)?,
|
||
path: row.get(16)?,
|
||
alter_id: row.get(17)?,
|
||
alter_id_i: None,
|
||
})
|
||
})
|
||
.map_err(|e| format!("读取失败: {e}"))?
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|e| format!("数据解析失败: {e}"))?;
|
||
|
||
// 清理临时文件
|
||
let _ = std::fs::remove_file(&tmp);
|
||
|
||
Ok(nodes)
|
||
}
|
||
|
||
/// 返回当前探测到的 v2rayN DB 路径(用于前端展示)
|
||
#[tauri::command]
|
||
pub fn detect_v2rayn_db_path() -> Option<String> {
|
||
find_v2rayn_db().map(|p| p.to_string_lossy().to_string())
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
pub struct ActiveV2raynNode {
|
||
pub index_id: String,
|
||
pub remarks: String,
|
||
pub address: String,
|
||
pub port: i64,
|
||
pub config_type: i64,
|
||
pub share_link: String,
|
||
}
|
||
|
||
/// 读取 v2rayN 当前激活节点,返回节点信息和 share link
|
||
#[tauri::command]
|
||
pub fn get_active_v2rayn_node(db_path: Option<String>) -> Result<ActiveV2raynNode, String> {
|
||
// 1. 找到 DB 路径
|
||
let db_file = match db_path {
|
||
Some(ref p) => PathBuf::from(p),
|
||
None => find_v2rayn_db().ok_or("未找到 v2rayN 数据库,请先配置路径")?,
|
||
};
|
||
|
||
// 2. 找同目录的 guiNConfig.json,读取当前激活 IndexId
|
||
let config_file = db_file
|
||
.parent()
|
||
.ok_or("无法获取 DB 目录")?
|
||
.join("guiNConfig.json");
|
||
|
||
let config_str = std::fs::read_to_string(&config_file)
|
||
.map_err(|e| format!("读取 guiNConfig.json 失败: {e}"))?;
|
||
|
||
#[derive(Deserialize)]
|
||
struct GuiConfig {
|
||
#[serde(rename = "IndexId")]
|
||
index_id: String,
|
||
}
|
||
let gui_cfg: GuiConfig =
|
||
serde_json::from_str(&config_str).map_err(|e| format!("解析 guiNConfig.json 失败: {e}"))?;
|
||
|
||
let active_id = gui_cfg.index_id;
|
||
|
||
// 3. 从 DB 查该节点详情
|
||
let tmp = copy_to_temp(&db_file)?;
|
||
let conn = Connection::open_with_flags(&tmp, OpenFlags::SQLITE_OPEN_READ_ONLY)
|
||
.map_err(|e| format!("打开 DB 失败: {e}"))?;
|
||
|
||
let node = conn
|
||
.query_row(
|
||
"SELECT IndexId, ConfigType, Remarks, Address, Port, Id, Security, Network,
|
||
StreamSecurity, Flow, Sni, Fingerprint, PublicKey, ShortId, SpiderX,
|
||
RequestHost, Path, AlterId
|
||
FROM ProfileItem WHERE IndexId = ?1",
|
||
[&active_id],
|
||
|row| {
|
||
Ok(V2raynNode {
|
||
index_id: row.get(0)?,
|
||
config_type: row.get(1)?,
|
||
remarks: row.get::<_, Option<String>>(2)?.unwrap_or_default(),
|
||
address: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||
port: row.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||
id: row.get(5)?,
|
||
security: row.get(6)?,
|
||
network: row.get(7)?,
|
||
stream_security: row.get(8)?,
|
||
flow: row.get(9)?,
|
||
sni: row.get(10)?,
|
||
fingerprint: row.get(11)?,
|
||
public_key: row.get(12)?,
|
||
short_id: row.get(13)?,
|
||
spider_x: row.get(14)?,
|
||
request_host: row.get(15)?,
|
||
path: row.get(16)?,
|
||
alter_id: row.get(17)?,
|
||
alter_id_i: None,
|
||
})
|
||
},
|
||
)
|
||
.map_err(|_| format!("未在 DB 中找到激活节点 {active_id}"))?;
|
||
|
||
let _ = std::fs::remove_file(&tmp);
|
||
|
||
// 4. 生成 share link
|
||
let share_link = build_share_link_rust(&node);
|
||
|
||
Ok(ActiveV2raynNode {
|
||
index_id: node.index_id,
|
||
remarks: node.remarks,
|
||
address: node.address,
|
||
port: node.port,
|
||
config_type: node.config_type,
|
||
share_link,
|
||
})
|
||
}
|
||
|
||
/// 在 Rust 侧生成 share link(与前端 buildShareLink 逻辑一致)
|
||
fn build_share_link_rust(n: &V2raynNode) -> String {
|
||
let remarks_enc = urlencoding::encode(&n.remarks).into_owned();
|
||
match n.config_type {
|
||
// VMess
|
||
1 => {
|
||
let obj = serde_json::json!({
|
||
"v": "2", "ps": n.remarks, "add": n.address, "port": n.port.to_string(),
|
||
"id": n.id.as_deref().unwrap_or(""),
|
||
"aid": n.alter_id.unwrap_or(0).to_string(),
|
||
"net": n.network.as_deref().unwrap_or("tcp"),
|
||
"tls": n.stream_security.as_deref().unwrap_or(""),
|
||
"sni": n.sni.as_deref().unwrap_or(""),
|
||
"host": n.request_host.as_deref().unwrap_or(""),
|
||
"path": n.path.as_deref().unwrap_or(""),
|
||
"type": "none",
|
||
});
|
||
let b64 =
|
||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, obj.to_string());
|
||
format!("vmess://{b64}")
|
||
}
|
||
// VLESS
|
||
5 => {
|
||
let mut params = format!(
|
||
"security={}&encryption=none",
|
||
n.stream_security.as_deref().unwrap_or("none")
|
||
);
|
||
if let Some(f) = &n.flow {
|
||
if !f.is_empty() {
|
||
params += &format!("&flow={f}");
|
||
}
|
||
}
|
||
if let Some(s) = &n.sni {
|
||
if !s.is_empty() {
|
||
params += &format!("&sni={s}");
|
||
}
|
||
}
|
||
if let Some(fp) = &n.fingerprint {
|
||
if !fp.is_empty() {
|
||
params += &format!("&fp={fp}");
|
||
}
|
||
}
|
||
if let Some(pk) = &n.public_key {
|
||
if !pk.is_empty() {
|
||
params += &format!("&pbk={pk}");
|
||
}
|
||
}
|
||
if let Some(sid) = &n.short_id {
|
||
if !sid.is_empty() {
|
||
params += &format!("&sid={sid}");
|
||
}
|
||
}
|
||
if let Some(net) = &n.network {
|
||
if !net.is_empty() {
|
||
params += &format!("&type={net}");
|
||
}
|
||
}
|
||
format!(
|
||
"vless://{}@{}:{}?{params}#{remarks_enc}",
|
||
n.id.as_deref().unwrap_or(""),
|
||
n.address,
|
||
n.port
|
||
)
|
||
}
|
||
// Trojan
|
||
3 => {
|
||
let mut params = String::new();
|
||
if let Some(s) = &n.sni {
|
||
if !s.is_empty() {
|
||
params = format!("sni={s}");
|
||
}
|
||
}
|
||
let q = if params.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("?{params}")
|
||
};
|
||
format!(
|
||
"trojan://{}@{}:{}{}#{remarks_enc}",
|
||
n.id.as_deref().unwrap_or(""),
|
||
n.address,
|
||
n.port,
|
||
q
|
||
)
|
||
}
|
||
// Shadowsocks
|
||
6 => {
|
||
let method = n.security.as_deref().unwrap_or("chacha20-poly1305");
|
||
let userinfo = base64::Engine::encode(
|
||
&base64::engine::general_purpose::STANDARD,
|
||
format!("{method}:{}", n.id.as_deref().unwrap_or("")),
|
||
);
|
||
format!("ss://{userinfo}@{}:{}#{remarks_enc}", n.address, n.port)
|
||
}
|
||
_ => format!("unsupported://{}:{}", n.address, n.port),
|
||
}
|
||
}
|