diff --git a/Cargo.toml b/Cargo.toml index 8cac4b4..93b3367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["switch","switch-desktop","switch-mini","switch-jni"] +members = ["switch","common","switch-desktop","switch-cmd","switch-jni"] [profile.release] opt-level = 'z' diff --git a/README.md b/README.md index d879491..ffedc95 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ 1. 指定一个token,在多台设备上运行该程序,例如: ```shell # linux上 - root@DESKTOP-0BCHNIO:/opt# ./switch-desktop start --token 123456 + root@DESKTOP-0BCHNIO:/opt# ./switch-cmd start --token 123456 # 在另一台linux上使用nohup后台运行 - [root@izj6cemne76ykdzkataftfz switch]# nohup ./switch-desktop start --token 123456 & + root@izj6cemne76ykdzkataftfz switch# nohup ./switch-cmd start --token 123456 & # windows上 - D:\switch\bin_v1>switch-desktop.exe start --token 123456 + D:\switch\bin_v1>switch-cmd.exe start --token 123456 ``` -2. 可以执行status命令查看当前设备的虚拟ip +2. 可以执行info命令查看当前设备的虚拟ip ```shell - root@DESKTOP-0BCHNIO:/opt# ./switch-desktop status + root@DESKTOP-0BCHNIO:/opt# ./switch-cmd --info Name: Ubuntu 18.04 (bionic) [64-bit] Virtual ip: 10.26.0.2 Virtual gateway: 10.26.0.1 @@ -30,7 +30,7 @@ ``` 3. 也可以执行list命令查看其他设备的虚拟ip ```shell - root@DESKTOP-0BCHNIO:/opt# ./switch-desktop list + root@DESKTOP-0BCHNIO:/opt# ./switch-cmd --list Name Virtual Ip P2P/Relay Rt Status Windows 10.0.22621 (Windows 11 Professional) [64-bit] 10.26.0.3 p2p 2 Online CentOS 7.9.2009 (Core) [64-bit] 10.26.0.4 p2p 35 Online @@ -54,7 +54,7 @@ ### 使用须知 - token的作用是标识一个虚拟局域网,当使用公共服务器时,建议使用一个唯一值当token(比如uuid),否则有可能连接到其他人创建的虚拟局域网中 -- 建议指定deviceId,默认使用MAC地址,在某些环境下可能发生变化 +- 建议指定deviceId,默认使用MAC地址,在某些环境下可能发生变化,**注意不要重复** - 公共服务器目前的配置是2核4G 4Mbps,有需要再扩展~ - 需要root/管理员权限 - 使用命令行运行 @@ -63,7 +63,7 @@ ### 编译 前提条件:安装rust编译环境(https://www.rust-lang.org/zh-CN/tools/install) - 到项目根目录下执行 cargo build -p switch-desktop + 到项目根目录下执行 cargo build -p switch-cmd ### 支持平台 - Mac diff --git a/switch-desktop/Cargo.toml b/switch-desktop/Cargo.toml index 0de5a18..affae88 100644 --- a/switch-desktop/Cargo.toml +++ b/switch-desktop/Cargo.toml @@ -1,38 +1,37 @@ [package] name = "switch-desktop" -version = "1.0.7" +version = "1.1.0" +description = "switch desktop" +authors = ["lubeilin"] +license = "" +repository = "" +default-run = "switch-desktop" edition = "2021" +rust-version = "1.60" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[build-dependencies] +tauri-build = { version = "1.4.0", features = [] } +embed-resource = "2.2" + [dependencies] switch = {path="../switch"} -mac_address = "1.1.4" -clap = { version = "4.0.32", features = ["derive"] } -console = "0.15.2" +common = {path="../common"} dirs = "4.0.0" -log = "0.4.17" -log4rs = "1.2.0" -tokio = { version = "1.28.1", features = ["full"] } -chrono = "0.4.23" - -rust-i18n = "1.2.2" -serde = "1.0" -serde_yaml = "0.9" -serde_json = "1.0.94" -crossbeam = "0.8.2" +os_info = "3.7.0" lazy_static = "1.4.0" parking_lot = "0.12.1" -fs2 = "0.4.3" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +tauri = { version = "1.4.0", features = [] } -os_info = "3.5.1" -[target.'cfg(any(target_os = "linux",target_os = "macos"))'.dependencies] -sudo = "0.6.0" -libc = "0.2" - -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "0.3.9", features = ["handleapi", "processthreadsapi", "winnt", "securitybaseapi", "impl-default"] } -#runas = "0.2.1" -windows-service = "0.6.0" +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = [] } +[features] +# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. +# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. +# DO NOT REMOVE!! +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/switch-desktop/README.md b/switch-desktop/README.md new file mode 100644 index 0000000..e69de29 diff --git a/switch-desktop/build.rs b/switch-desktop/build.rs new file mode 100644 index 0000000..8c8da1a --- /dev/null +++ b/switch-desktop/build.rs @@ -0,0 +1,21 @@ +fn main() { + if std::env::var_os("CARGO_CFG_WINDOWS").is_some() { + let mut windows = tauri_build::WindowsAttributes::new(); + windows = windows.app_manifest(r#" + + + + + + + + + + +"#); + let attrs = tauri_build::Attributes::new().windows_attributes(windows); + tauri_build::try_build(attrs).expect("failed to run build script"); + } else { + tauri_build::build(); + } +} diff --git a/switch-desktop/dll/amd64/wintun.dll b/switch-desktop/dll/amd64/wintun.dll new file mode 100644 index 0000000..9f1a181 Binary files /dev/null and b/switch-desktop/dll/amd64/wintun.dll differ diff --git a/switch-desktop/icons/128x128.png b/switch-desktop/icons/128x128.png new file mode 100644 index 0000000..77e7d23 Binary files /dev/null and b/switch-desktop/icons/128x128.png differ diff --git a/switch-desktop/icons/128x128@2x.png b/switch-desktop/icons/128x128@2x.png new file mode 100644 index 0000000..0f7976f Binary files /dev/null and b/switch-desktop/icons/128x128@2x.png differ diff --git a/switch-desktop/icons/32x32.png b/switch-desktop/icons/32x32.png new file mode 100644 index 0000000..98fda06 Binary files /dev/null and b/switch-desktop/icons/32x32.png differ diff --git a/switch-desktop/icons/Square107x107Logo.png b/switch-desktop/icons/Square107x107Logo.png new file mode 100644 index 0000000..f35d84f Binary files /dev/null and b/switch-desktop/icons/Square107x107Logo.png differ diff --git a/switch-desktop/icons/Square142x142Logo.png b/switch-desktop/icons/Square142x142Logo.png new file mode 100644 index 0000000..1823bb2 Binary files /dev/null and b/switch-desktop/icons/Square142x142Logo.png differ diff --git a/switch-desktop/icons/Square150x150Logo.png b/switch-desktop/icons/Square150x150Logo.png new file mode 100644 index 0000000..dc2b22c Binary files /dev/null and b/switch-desktop/icons/Square150x150Logo.png differ diff --git a/switch-desktop/icons/Square284x284Logo.png b/switch-desktop/icons/Square284x284Logo.png new file mode 100644 index 0000000..0ed3984 Binary files /dev/null and b/switch-desktop/icons/Square284x284Logo.png differ diff --git a/switch-desktop/icons/Square30x30Logo.png b/switch-desktop/icons/Square30x30Logo.png new file mode 100644 index 0000000..60bf0ea Binary files /dev/null and b/switch-desktop/icons/Square30x30Logo.png differ diff --git a/switch-desktop/icons/Square310x310Logo.png b/switch-desktop/icons/Square310x310Logo.png new file mode 100644 index 0000000..c8ca0ad Binary files /dev/null and b/switch-desktop/icons/Square310x310Logo.png differ diff --git a/switch-desktop/icons/Square44x44Logo.png b/switch-desktop/icons/Square44x44Logo.png new file mode 100644 index 0000000..8756459 Binary files /dev/null and b/switch-desktop/icons/Square44x44Logo.png differ diff --git a/switch-desktop/icons/Square71x71Logo.png b/switch-desktop/icons/Square71x71Logo.png new file mode 100644 index 0000000..2c8023c Binary files /dev/null and b/switch-desktop/icons/Square71x71Logo.png differ diff --git a/switch-desktop/icons/Square89x89Logo.png b/switch-desktop/icons/Square89x89Logo.png new file mode 100644 index 0000000..2c5e603 Binary files /dev/null and b/switch-desktop/icons/Square89x89Logo.png differ diff --git a/switch-desktop/icons/StoreLogo.png b/switch-desktop/icons/StoreLogo.png new file mode 100644 index 0000000..17d142c Binary files /dev/null and b/switch-desktop/icons/StoreLogo.png differ diff --git a/switch-desktop/icons/icon.icns b/switch-desktop/icons/icon.icns new file mode 100644 index 0000000..a2993ad Binary files /dev/null and b/switch-desktop/icons/icon.icns differ diff --git a/switch-desktop/icons/icon.ico b/switch-desktop/icons/icon.ico new file mode 100644 index 0000000..06c23c8 Binary files /dev/null and b/switch-desktop/icons/icon.ico differ diff --git a/switch-desktop/icons/icon.png b/switch-desktop/icons/icon.png new file mode 100644 index 0000000..d1756ce Binary files /dev/null and b/switch-desktop/icons/icon.png differ diff --git a/switch-desktop/locales/en.yml b/switch-desktop/locales/en.yml deleted file mode 100644 index cc67be2..0000000 --- a/switch-desktop/locales/en.yml +++ /dev/null @@ -1,51 +0,0 @@ -switch_about: "A virtual network tool that will obtain an ip after startup. Devices under the same token will form a virtual local area network, and can use ip to communicate directly with each other." -switch_usage: "switch-desktop.exe " -switch_start_about: "Start switch" -switch_token_help: "Using the same token, you can build a local area network. It is recommended to use a more complex token to avoid connecting to other people's local area network" -switch_name_help: "Give the device a name, the system version information will be used by default" -switch_device_id_help: "The unique identifier of the device, the ip is assigned according to the id, and the MAC address is used by default" -switch_server_help: "Registry and relay server address, public server is used by default" -switch_nat_test_server_help: "NAT detection server addresses, separated by commas" -switch_log_help: "Record the log, the output is in the '${home}/.switch_desktop' directory, it is not recommended to open it for long-term use" -switch_tap_help: "Use tap mode, tun mode will be used by default" -switch_in_ip_help: "Use when configuring point-to-network (IP proxy), --in-ip 192.168.10.0/24,10.26.0.3, which means it is allowed to receive data from the network segment 192.168.10.0/24 and forward it to 10.26.0.3" -switch_out_ip_help: "Use when configuring point-to-network, --out-ip 192.168.10.0/24,192.168.1.10, which means that the data with the target of 192.168.10.0/24 is allowed to be forwarded from the network card 192.168.1.10" -switch_password_help: "Client Data Encryption" -switch_simulate_multicast_help: "Simulate multicast. By default, multicast data will be sent as broadcast, which is more compatible, but it will cause traffic waste. After it is turned on, it will simulate real multicast data transmission" -switch_config_help: "Read configuration file" -switch_stop_about: "Stop background service" -switch_route_about: "View route" -switch_list_about: "View device list" -switch_list_all_help: "View full information" -switch_status_about: "View current device information" -switch_install_about: "Install windows service" -switch_path_help: "Service installation path, it is recommended to use an empty directory" -switch_auto_help: "Service starts automatically at boot" -switch_uninstall_about: "Uninstall windows service" -switch_config_about: "Change Windows Service Configuration" - -switch_use_admin_print: "Please run with administrator privileges" -switch_use_root_print: "Please run with root privileges" -switch_service_not_start_print: "service not started" -switch_start_successfully_print: "Start successfully" -switch_start_failed_print: "Startup failed" -switch_service_not_stopped_print: "service not stopped" -switch_stopped_print: "stopped" -switch_server_already_installed_print: "service is installed" - - -switch_repeated_start_print: "cannot be restarted repeatedly" - -switch_token_not_found_print: "missing token" -switch_token_cannot_be_empty_print: "token cannot be empty" -switch_token_cannot_exceed_64_print: "token cannot exceed 64 characters" -switch_device_id_is_empty_print: "The device id cannot be empty and the length cannot be greater than 64 characters" -switch_in_ips_example_print: "in_ips Parameter error Example:--in_ip 192.168.10.0/24,10.26.0.3" -switch_out_ips_example_print: "out_ips Parameter error Example:--out_ip 192.168.10.0/24,192.168.0.5" -switch_relay_server_address_error: "Wrong relay server address" -switch_nat_test_server_address_error: "NAT detection service address error" -switch_press_any_key_to_exit: "Press any key to exit" - -switch_virtual_ip: "virtual ip" -switch_virtual_gateway: "virtual gateway" -switch_please_enter_the_command: "Please enter the command (Usage: list,route,status,exit,help):" \ No newline at end of file diff --git a/switch-desktop/locales/zh-CN.yml b/switch-desktop/locales/zh-CN.yml deleted file mode 100644 index 1d7f378..0000000 --- a/switch-desktop/locales/zh-CN.yml +++ /dev/null @@ -1,51 +0,0 @@ -switch_about: "一个虚拟网络工具,启动后会获取一个ip,相同token下的设备会组件虚拟局域网,之间可以用ip直接通信" -switch_usage: "switch-desktop.exe <命令>" -switch_start_about: "启动switch" -switch_token_help: "使用相同的token,就能组建一个局域网络,建议使用一个复杂一点的token,避免连到其他人的局域网中" -switch_name_help: "给设备一个名字,默认会使用系统版本信息" -switch_device_id_help: "设备唯一标识符,凭id分配ip,默认使用MAC地址" -switch_server_help: "注册和中继服务器地址,默认使用公共服务器" -switch_nat_test_server_help: "NAT探测服务器地址,使用逗号分隔" -switch_log_help: "记录日志,输出在 '${home}/.switch_desktop' 目录下,长时间使用时不建议开启" -switch_tap_help: "使用tap模式,默认会使用tun模式" -switch_in_ip_help: "配置点对网(IP代理)时使用,--in-ip 192.168.10.0/24,10.26.0.3,表示允许接收网段192.168.10.0/24的数据并转发到10.26.0.3" -switch_out_ip_help: "配置点对网时使用,--out-ip 192.168.10.0/24,192.168.1.10,表示允许目标为192.168.10.0/24的数据从网卡192.168.1.10转发出去" -switch_password_help: "使用该密码生成的密钥对客户端数据进行加密,并且服务端无法解密。使用相同密码的客户端才能通信" -switch_simulate_multicast_help: "模拟组播,默认情况下组播数据会被当作广播发送,兼容性更强,但是会造成流量浪费。开启后会模拟真实组播的数据发送" -switch_config_help: "读取配置文件" -switch_stop_about: "停止后台服务" -switch_route_about: "查看路由" -switch_list_about: "查看设备列表" -switch_list_all_help: "查看完整信息" -switch_status_about: "查看当前设备信息" -switch_install_about: "安装Windows服务" -switch_path_help: "服务安装路径,建议使用一个空目录" -switch_auto_help: "服务开机自启动" -switch_uninstall_about: "卸载Windows服务" -switch_config_about: "改变Windows服务配置" - -switch_use_admin_print: "请使用管理员权限运行" -switch_use_root_print: "请使用root权限运行" -switch_service_not_start_print: "服务未启动" -switch_start_successfully_print: "启动成功" -switch_start_failed_print: "启动失败" -switch_service_not_stopped_print: "服务未停止" -switch_stopped_print: "已停止" -switch_server_already_installed_print: "服务已经安装" - - -switch_repeated_start_print: "不能重复启动" - -switch_token_not_found_print: "缺少token" -switch_token_cannot_be_empty_print: "token不能为空" -switch_token_cannot_exceed_64_print: "token不能超过64个字符" -switch_device_id_is_empty_print: "设备id不能为空并且长度不能大于64字符" -switch_in_ips_example_print: "in_ips 参数错误 示例:--in_ip 192.168.10.0/24,10.26.0.3" -switch_out_ips_example_print: "out_ips 参数错误 示例:--out_ip 192.168.10.0/24,192.168.0.5" -switch_relay_server_address_error: "中继服务器地址错误" -switch_nat_test_server_address_error: "NAT检测服务地址错误" -switch_press_any_key_to_exit: "按任意键退出" - -switch_virtual_ip: "当前虚拟ip(virtual ip)" -switch_virtual_gateway: "虚拟网关(virtual gateway)" -switch_please_enter_the_command: "输入命令 (例如: list,route,status,exit,help):" \ No newline at end of file diff --git a/switch-desktop/src/command/client.rs b/switch-desktop/src/command/client.rs deleted file mode 100644 index 765588c..0000000 --- a/switch-desktop/src/command/client.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::io; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; -use std::time::Duration; - -use crate::command::entity::{DeviceItem, RouteItem, Status}; - -pub struct CommandClient { - udp: UdpSocket, -} - -impl CommandClient { - pub fn new() -> io::Result { - let port = crate::config::read_command_port()?; - let udp = UdpSocket::bind("127.0.0.1:0")?; - udp.set_read_timeout(Some(Duration::from_secs(2)))?; - udp.connect(SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(127, 0, 0, 1), - port, - )))?; - Ok(Self { udp }) - } -} - -impl CommandClient { - pub fn list(&self) -> io::Result> { - self.udp.send(b"list")?; - let mut buf = [0; 10240]; - let len = self.udp.recv(&mut buf)?; - match serde_json::from_slice::>(&buf[..len]) { - Ok(val) => { - Ok(val) - } - Err(e) => { - log::error!("{:?}",e); - Err(io::Error::new(io::ErrorKind::Other, "data error")) - } - } - } - pub fn route(&self) -> io::Result> { - self.udp.send(b"route")?; - let mut buf = [0; 10240]; - let len = self.udp.recv(&mut buf)?; - match serde_json::from_slice::>(&buf[..len]) { - Ok(val) => { - Ok(val) - } - Err(e) => { - log::error!("{:?}",e); - Err(io::Error::new(io::ErrorKind::Other, "data error")) - } - } - } - pub fn status(&self) -> io::Result { - self.udp.send(b"status")?; - let mut buf = [0; 10240]; - let len = self.udp.recv(&mut buf)?; - match serde_json::from_slice::(&buf[..len]) { - Ok(val) => { - Ok(val) - } - Err(e) => { - log::error!("{:?},{:?}",&buf[..len],e); - Err(io::Error::new(io::ErrorKind::Other, "data error")) - } - } - } - #[cfg(any(unix))] - pub fn stop(&self) -> io::Result { - self.udp.send(b"stop")?; - let mut buf = [0; 10240]; - let len = self.udp.recv(&mut buf)?; - Ok(String::from_utf8(buf[..len].to_vec()).unwrap()) - } -} diff --git a/switch-desktop/src/command/entity.rs b/switch-desktop/src/command/entity.rs deleted file mode 100644 index 2f7c373..0000000 --- a/switch-desktop/src/command/entity.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] -pub struct Status { - pub name: String, - pub virtual_ip: String, - pub virtual_gateway: String, - pub virtual_netmask: String, - pub connect_status: String, - pub relay_server: String, - pub nat_type: String, - pub public_ips: String, - pub local_ip: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct RouteItem { - pub destination: String, - pub next_hop: String, - pub metric: String, - pub rt: String, - pub interface: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DeviceItem { - pub name: String, - pub virtual_ip: String, - pub nat_type: String, - pub public_ips: String, - pub local_ip: String, - pub nat_traversal_type: String, - pub rt: String, - pub status: String, -} \ No newline at end of file diff --git a/switch-desktop/src/command/mod.rs b/switch-desktop/src/command/mod.rs deleted file mode 100644 index ab99755..0000000 --- a/switch-desktop/src/command/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::io; -use console::style; -use crate::console_out; - -pub mod client; -pub mod server; -pub mod entity; - -pub enum CommandEnum { - Route, - List, - ListAll, - Status, - #[cfg(any(unix))] - Stop, -} - -pub fn command(cmd: CommandEnum) { - if let Err(e) = command_(cmd) { - println!("{}:{:?}", style("连接后台服务错误(Connection background service error)").red(), e); - } -} - -fn command_(cmd: CommandEnum) -> io::Result<()> { - match client::CommandClient::new() { - Ok(command_client) => { - match cmd { - CommandEnum::Route => { - let list = command_client.route()?; - console_out::console_route_table(list); - } - CommandEnum::List => { - let list = command_client.list()?; - console_out::console_device_list(list); - } - CommandEnum::ListAll => { - let list = command_client.list()?; - console_out::console_device_list_all(list); - } - CommandEnum::Status => { - let status = command_client.status()?; - console_out::console_status(status); - } - #[cfg(any(unix))] - CommandEnum::Stop => { - command_client.stop()?; - } - } - } - Err(e) => { - log::error!("{:?}",e); - println!( - "{}:{:?}", - style("连接后台服务错误(Connection background service error)").red(), e - ); - } - }; - Ok(()) -} diff --git a/switch-desktop/src/command/server.rs b/switch-desktop/src/command/server.rs deleted file mode 100644 index 3c0fea4..0000000 --- a/switch-desktop/src/command/server.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::io; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; -use std::sync::Arc; - -use switch::core::Switch; -use crate::command::entity::{DeviceItem, RouteItem, Status}; - - -pub struct CommandServer {} - -impl CommandServer { - pub fn new() -> Self { - Self {} - } -} - -impl CommandServer { - pub fn start(&self, switch: Arc) -> io::Result<()> { - let mut port = 21637 as u16; - let udp = loop { - match UdpSocket::bind(SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(127, 0, 0, 1), - port, - ))) { - Ok(udp) => { - break udp; - } - Err(e) => { - if e.kind() == io::ErrorKind::AddrInUse { - port += 1; - } else { - log::error!("创建udp失败 {:?}", e); - return Err(e); - } - } - } - }; - crate::config::update_command_port(port)?; - let mut buf = [0u8; 64]; - loop { - let (len, addr) = udp.recv_from(&mut buf)?; - match std::str::from_utf8(&buf[..len]) { - Ok(cmd) => { - if let Ok(out) = command(cmd, &switch) { - udp.send_to(out.as_bytes(), addr)?; - } - } - Err(e) => { - log::warn!("{:?}", e); - } - } - } - } -} - -pub fn command_route(switch: &Switch) -> Vec { - let route_table = switch.route_table(); - let mut route_list = Vec::with_capacity(route_table.len()); - for (destination, route) in route_table { - let next_hop = switch.route_key(&route.route_key()).map_or(String::new(), |v| v.to_string()); - let metric = route.metric.to_string(); - let rt = if route.rt < 0 { - "".to_string() - } else { - route.rt.to_string() - }; - let interface = route.addr.to_string(); - let item = RouteItem { - destination: destination.to_string(), - next_hop, - metric, - rt, - interface, - }; - route_list.push(item); - } - route_list -} - -pub fn command_list(switch: &Switch) -> Vec { - let device_list = switch.device_list(); - let mut list = Vec::new(); - for peer in device_list { - let name = peer.name; - let virtual_ip = peer.virtual_ip.to_string(); - let (nat_type, public_ips, local_ip) = if let Some(nat_info) = switch.peer_nat_info(&peer.virtual_ip) { - let nat_type = format!("{:?}", nat_info.nat_type); - let public_ips: Vec = nat_info.public_ips.iter().map(|v| v.to_string()).collect(); - let public_ips = public_ips.join(","); - let local_ip = nat_info.local_ip.to_string(); - (nat_type, public_ips, local_ip) - } else { - ("".to_string(), "".to_string(), "".to_string()) - }; - let (nat_traversal_type, rt) = if let Some(route) = switch.route(&peer.virtual_ip) { - let nat_traversal_type = if route.metric == 1 { "p2p" } else { "relay" }.to_string(); - let rt = if route.rt < 0 { - "".to_string() - } else { - route.rt.to_string() - }; - (nat_traversal_type, rt) - } else { - ("relay".to_string(), "".to_string()) - }; - let status = format!("{:?}", peer.status); - let item = DeviceItem { - name, - virtual_ip, - nat_type, - public_ips, - local_ip, - nat_traversal_type, - rt, - status, - }; - list.push(item); - } - list -} - -pub fn command_status(switch: &Switch) -> Status { - let current_device = switch.current_device(); - let nat_info = switch.nat_info(); - let name = switch.name().to_string(); - let virtual_ip = current_device.virtual_ip().to_string(); - let virtual_gateway = current_device.virtual_gateway().to_string(); - let virtual_netmask = current_device.virtual_netmask.to_string(); - let connect_status = format!("{:?}", switch.connection_status()); - let relay_server = current_device.connect_server.to_string(); - let nat_type = format!("{:?}", nat_info.nat_type); - let public_ips: Vec = nat_info.public_ips.iter().map(|v| v.to_string()).collect(); - let public_ips = public_ips.join(","); - let local_ip = nat_info.local_ip.to_string(); - Status { - name, - virtual_ip, - virtual_gateway, - virtual_netmask, - connect_status, - relay_server, - nat_type, - public_ips, - local_ip, - } -} - -fn command(cmd: &str, switch: &Switch) -> io::Result { - let out_str = match cmd { - "route" => { - match serde_json::to_string(&command_route(switch)) { - Ok(str) => { - str - } - Err(e) => { - format!("{:?}", e) - } - } - } - "list" => { - match serde_json::to_string(&command_list(switch)) { - Ok(str) => { - str - } - Err(e) => { - format!("{:?}", e) - } - } - } - "status" => { - match serde_json::to_string(&command_status(switch)) { - Ok(str) => { - str - } - Err(e) => { - format!("{:?}", e) - } - } - } - "stop" => { - switch.stop()?; - "stopping".to_string() - } - _ => { - format!("command '{}' not found. \n Try to enter: 'help'\n", cmd) - } - }; - Ok(out_str) -} diff --git a/switch-desktop/src/command_args.rs b/switch-desktop/src/command_args.rs deleted file mode 100644 index 3c78032..0000000 --- a/switch-desktop/src/command_args.rs +++ /dev/null @@ -1,140 +0,0 @@ -use clap::{Arg, ArgAction, Command}; -use clap::builder::BoolishValueParser; -use crate::i18n::*; - -fn common() -> Command { - Command::new("switch-desktop") - .about(switch_about()) - // .version(switch_version()) - .subcommand_required(true) - .arg_required_else_help(true) - // .author(switch_author()) - .override_usage(switch_usage()) - .subcommand( - Command::new("start") - .about(switch_start_about()) - .arg( - Arg::new("token") - .long("token") - .help(switch_token_help()) - .action(ArgAction::Set) - ) - .arg( - Arg::new("name") - .long("name") - .help(switch_name_help()) - .action(ArgAction::Set) - ) - .arg( - Arg::new("device_id") - .long("device-id") - .help(switch_device_id_help()) - .action(ArgAction::Set) - ).arg( - Arg::new("server") - .long("server") - .help(switch_server_help()) - .action(ArgAction::Set) - ).arg( - Arg::new("nat_test_server") - .long("nat-test-server") - .help(switch_nat_test_server_help()) - .action(ArgAction::Set) - ).arg( - Arg::new("log") - .long("log") - .help(switch_log_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), - ).arg( - Arg::new("tap") - .long("tap") - .help(switch_tap_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), - ).arg( - Arg::new("in_ip") - .long("in-ip") - .help(switch_in_ip_help()) - .action(ArgAction::Append) - .num_args(1..), - ).arg( - Arg::new("out_ip") - .long("out-ip") - .help(switch_out_ip_help()) - .action(ArgAction::Append) - ).arg( - Arg::new("password") - .long("password") - .help(switch_password_help()) - .action(ArgAction::Set) - ).arg( - Arg::new("simulate_multicast") - .long("simulate-multicast") - .help(switch_simulate_multicast_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), - ).arg( - Arg::new("config") - .long("config") - .help(switch_config_help()) - .action(ArgAction::Set) - ) - , - ).subcommand( - Command::new("stop") - .about(switch_stop_about())) - .subcommand( - Command::new("route") - .about(switch_route_about())) - .subcommand(Command::new("list") - .about(switch_list_about()).arg( - Arg::new("all") - .long("all") - .short('a') - .help(switch_list_all_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), )) - .subcommand(Command::new("status") - .about(switch_status_about())) -} - -pub fn check() -> bool { - #[cfg(windows)] - let cmd = common().subcommand(Command::new("install") - .about(switch_install_about()) - .arg( - Arg::new("path") - .long("path") - .help(switch_path_help()) - .action(ArgAction::Set) - .num_args(1..)) - .arg( - Arg::new("auto") - .long("auto") - .help(switch_auto_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), )) - .subcommand(Command::new("uninstall") - .about(switch_uninstall_about())) - .subcommand(Command::new("config") - .about(switch_config_about()) - .arg( - Arg::new("auto") - .long("auto") - .help(switch_auto_help()) - .action(ArgAction::SetTrue) - .value_parser(BoolishValueParser::new()), )); - #[cfg(any(unix))] - let cmd = common(); - match cmd.try_get_matches() { - Ok(_) => { - true - } - Err(e) => { - println!("{}", e); - false - } - } -} - diff --git a/switch-desktop/src/config/log_config.rs b/switch-desktop/src/config/log_config.rs deleted file mode 100644 index 2386d59..0000000 --- a/switch-desktop/src/config/log_config.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::io; -use std::path::PathBuf; -use crate::config::get_home; - -#[cfg(target_os = "windows")] -pub fn log_service_init() -> io::Result<()> { - log_init_(crate::config::get_win_server_home().join("switch-service.log")) -} - -pub fn log_init() -> io::Result<()> { - log_init_(get_home().join("switch-desktop.log")) -} - -fn log_init_(file_name: PathBuf) -> io::Result<()> { - let stderr = log4rs::append::console::ConsoleAppender::builder() - .target(log4rs::append::console::Target::Stderr) - .build(); - let logfile = log4rs::append::file::FileAppender::builder() - // Pattern: https://docs.rs/log4rs/*/log4rs/encode/pattern/index.html - .encoder(Box::new(log4rs::encode::pattern::PatternEncoder::new( - "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}\n", - ))) - .build(file_name)?; - match log4rs::Config::builder() - .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile))) - .appender( - log4rs::config::Appender::builder() - .filter(Box::new(log4rs::filter::threshold::ThresholdFilter::new( - log::LevelFilter::Error, - ))) - .build("stderr", Box::new(stderr)), - ) - .build( - log4rs::config::Root::builder() - .appender("logfile") - .appender("stderr") - .build(log::LevelFilter::Info), - ) { - Ok(config) => { - let _ = log4rs::init_config(config); - } - Err(_) => {} - } - Ok(()) -} diff --git a/switch-desktop/src/config/mod.rs b/switch-desktop/src/config/mod.rs index 992ffbf..8c7518f 100644 --- a/switch-desktop/src/config/mod.rs +++ b/switch-desktop/src/config/mod.rs @@ -1,515 +1,9 @@ -use std::fs::{File, OpenOptions}; -use std::io; -use std::io::{Read, Write}; -use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs}; use std::path::PathBuf; -use lazy_static::lazy_static; -use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; - -use crate::{i18n, StartArgs}; - -pub mod log_config; -lazy_static! { - pub static ref SWITCH_HOME_PATH: Mutex> = Mutex::new(None); -} - -#[cfg(windows)] -pub fn get_win_server_home() -> PathBuf { - SWITCH_HOME_PATH.lock().as_ref().unwrap().clone() -} - -#[cfg(windows)] -pub fn set_win_server_home(home: PathBuf) { - let _ = SWITCH_HOME_PATH.lock().insert(home); -} - -#[derive(Clone, Debug)] -pub struct StartConfig { - pub tap: bool, - pub name: String, - pub token: String, - pub server: SocketAddr, - pub nat_test_server: Vec, - pub device_id: String, - pub in_ips: Vec<(u32, u32, Ipv4Addr)>, - pub out_ips: Vec<(u32, u32, Ipv4Addr)>, - #[cfg(any(unix))] - pub off_command_server: bool, - pub log: bool, - pub password: Option, - pub simulate_multicast:bool, -} - -fn ips_parse(ips: &Vec) -> Result, String> { - let mut in_ips_c = vec![]; - for x in ips { - let mut split = x.split(","); - let net = if let Some(net) = split.next() { - net - } else { - return Err("参数错误".to_string()); - }; - let ip = if let Some(ip) = split.next() { - ip - } else { - return Err("参数错误".to_string()); - }; - let ip = if let Ok(ip) = ip.parse::() { - ip - } else { - return Err("参数错误".to_string()); - }; - let mut split = net.split("/"); - let dest = if let Some(dest) = split.next() { - dest - } else { - return Err("参数错误".to_string()); - }; - let mask = if let Some(mask) = split.next() { - mask - } else { - return Err("参数错误".to_string()); - }; - let dest = if let Ok(dest) = dest.parse::() { - dest - } else { - return Err("参数错误".to_string()); - }; - let mask = if let Ok(m) = mask.parse::() { - let mut mask = 0 as u32; - for i in 0..m { - mask = mask | (1 << (31 - i)); - } - mask - } else { - return Err("参数错误".to_string()); - }; - in_ips_c.push((u32::from_be_bytes(dest.octets()), mask, ip)); - } - Ok(in_ips_c) -} - -pub fn default_config(start_args: StartArgs) -> Result { - println!("========参数配置========"); - if start_args.log { - println!("print log"); - } - let tap = start_args.tap; - if tap { - println!("use tap"); - } else { - println!("use tun"); - } - if start_args.token.is_none() { - return Err(i18n::switch_token_not_found_print()); - } - let token = start_args.token.unwrap(); - if token.is_empty() { - return Err(i18n::switch_token_cannot_be_empty_print()); - } - if token.len() > 64 { - return Err(i18n::switch_token_cannot_exceed_64_print()); - } - println!("token:{:?}", token); - let name = start_args.name.unwrap_or_else(|| { - os_info::get().to_string() - }); - let name = name.trim(); - let name = if name.len() > 64 { - name[..64].to_string() - } else { - name.to_string() - }; - println!("name:{:?}", name); - let device_id = start_args.device_id.unwrap_or_else(|| { - if let Ok(Some(mac_address)) = mac_address::get_mac_address() { - mac_address.to_string() - } else { - "".to_string() - } - }); - if device_id.is_empty() || device_id.len() > 64 { - return Err(i18n::switch_device_id_is_empty_print()); - } - println!("device_id:{:?}", device_id); - let in_ips = start_args.in_ip.unwrap_or_else(|| { - vec![] - }); - let out_ips = start_args.out_ip.unwrap_or_else(|| { - vec![] - }); - println!("in_ips:{:?}", in_ips); - let in_ips_c = if let Ok(in_ips_c) = ips_parse(&in_ips) { - in_ips_c - } else { - return Err(i18n::switch_in_ips_example_print()); - }; - println!("out_ips:{:?}", out_ips); - let out_ips_c = if let Ok(out_ips_c) = ips_parse(&out_ips) { - out_ips_c - } else { - return Err(i18n::switch_out_ips_example_print()); - }; - - - let server = match start_args.server.unwrap_or_else(|| { - "nat1.wherewego.top:29871".to_string() - }).to_socket_addrs() { - Ok(mut server) => { - if let Some(addr) = server.next() { - addr - } else { - return Err(i18n::switch_relay_server_address_error()); - } - } - Err(e) => { - return Err(format!("{} :{:?}", i18n::switch_relay_server_address_error(), e)); - } - }; - println!("中继服务器:{:?}", server); - let nat_test_server = start_args.nat_test_server.unwrap_or_else(|| { - "nat1.wherewego.top:35061,nat1.wherewego.top:35062,nat2.wherewego.top:35061,nat2.wherewego.top:35062".to_string() - }).split(",").flat_map(|a| a.to_socket_addrs()).flatten() - .collect::>(); - if nat_test_server.is_empty() { - return Err(i18n::switch_nat_test_server_address_error()); - } - println!("NAT探测服务器:{:?}", nat_test_server); - let base_config = StartConfig { - tap, - name, - token, - server, - nat_test_server, - device_id, - in_ips: in_ips_c, - out_ips: out_ips_c, - #[cfg(any(unix))] - off_command_server: start_args.off_command_server, - log: start_args.log, - password: start_args.password, - simulate_multicast:start_args.simulate_multicast, - }; - println!("========参数配置========"); - Ok(base_config) -} - -pub fn read_config_file(config_path: PathBuf) -> Result { - println!("========读取配置文件========"); - let args_config = if let Ok(config) = read_config(config_path) { - config - } else { - return Err("读取配置文件失败".to_string()); - }; - let log = args_config.log; - if log { - println!("print log"); - } - let tap = args_config.tap; - if tap { - println!("use tap"); - } else { - println!("use tun"); - } - let token = args_config.token; - if token.is_empty() { - return Err(i18n::switch_token_cannot_be_empty_print()); - } - if token.len() > 64 { - return Err(i18n::switch_token_cannot_exceed_64_print()); - } - println!("token:{:?}", token); - let name = args_config.name; - let name = name.trim(); - let name = if name.len() > 64 { - name[..64].to_string() - } else { - name.to_string() - }; - println!("name:{:?}", name); - let device_id = if !args_config.device_id.is_empty() { - args_config.device_id - } else { - if let Ok(Some(mac_address)) = mac_address::get_mac_address() { - mac_address.to_string() - } else { - "".to_string() - } - }; - if device_id.is_empty() || device_id.len() > 64 { - return Err(i18n::switch_device_id_is_empty_print()); - } - println!("device_id:{:?}", device_id); - let in_ips = args_config.in_ips; - let out_ips = args_config.out_ips; - println!("in_ips:{:?}", in_ips); - let in_ips_c = if let Ok(in_ips_c) = ips_parse(&in_ips) { - in_ips_c - } else { - return Err(i18n::switch_in_ips_example_print()); - }; - println!("out_ips:{:?}", out_ips); - let out_ips_c = if let Ok(out_ips_c) = ips_parse(&out_ips) { - out_ips_c - } else { - return Err(i18n::switch_out_ips_example_print()); - }; - let server = match { - if !args_config.server.is_empty() { - args_config.server - } else { - "nat1.wherewego.top:29871".to_string() - } - }.to_socket_addrs() - { - Ok(mut server) => { - if let Some(addr) = server.next() { - addr - } else { - return Err(i18n::switch_relay_server_address_error()); - } - } - Err(e) => { - return Err(format!("{}:{:?}", i18n::switch_relay_server_address_error(), e)); - } - }; - println!("中继服务器:{:?}", server); - let nat_test_server = if args_config.nat_test_server.is_empty() { - vec!["nat1.wherewego.top:35061".to_string(), "nat1.wherewego.top:35062".to_string(), "nat2.wherewego.top:35061".to_string(), "nat2.wherewego.top:35062".to_string()] - } else { - args_config.nat_test_server - }.iter().flat_map(|a| a.to_socket_addrs()).flatten() - .collect::>(); - if nat_test_server.is_empty() { - return Err(i18n::switch_nat_test_server_address_error()); - } - println!("NAT探测服务器:{:?}", nat_test_server); - let base_config = StartConfig { - tap, - name, - token, - server, - nat_test_server, - device_id, - in_ips: in_ips_c, - out_ips: out_ips_c, - #[cfg(any(unix))] - off_command_server: args_config.off_command_server, - log, - password:args_config.password, - simulate_multicast:args_config.simulate_multicast - }; - println!("========参数配置========"); - Ok(base_config) -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RuntimeData { - #[serde(default = "default_pid")] - pub pid: u32, - pub command_port: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ArgsConfig { - #[serde(default = "default_false")] - pub tap: bool, - #[serde(default = "default_version")] - pub version: String, - #[serde(default = "default_str")] - pub token: String, - #[serde(default = "default_str")] - pub name: String, - #[serde(default = "default_str")] - pub server: String, - #[serde(default = "default_vec")] - pub nat_test_server: Vec, - #[serde(default = "default_str")] - pub device_id: String, - #[serde(default = "default_vec")] - pub in_ips: Vec, - #[serde(default = "default_vec")] - pub out_ips: Vec, - #[cfg(any(unix))] - #[serde(default = "default_false")] - pub off_command_server: bool, - #[serde(default = "default_false")] - pub log: bool, - pub password: Option, - #[serde(default = "default_false")] - pub simulate_multicast:bool, -} - -#[cfg(windows)] -impl ArgsConfig { - pub fn new(start_config: StartConfig) -> ArgsConfig { - let in_ips = start_config.in_ips.iter().map(|(ip, mask, dest)| { - format!("{}/{},{}", Ipv4Addr::from(*ip), subnet_mask_to_integer(*mask), dest) - }).collect::>(); - let out_ips = start_config.out_ips.iter().map(|(ip, mask, dest)| { - format!("{}/{},{}", Ipv4Addr::from(*ip), subnet_mask_to_integer(*mask), dest) - }).collect::>(); - ArgsConfig { - tap: start_config.tap, - version: "1.0.6".to_string(), - token: start_config.token.to_string(), - name: start_config.name.to_string(), - server: start_config.server.to_string(), - nat_test_server: start_config.nat_test_server.iter().map(|v| v.to_string()).collect(), - device_id: start_config.device_id, - in_ips, - out_ips, - log: start_config.log, - #[cfg(any(unix))] - off_command_server: start_config.off_command_server, - password: start_config.password, - simulate_multicast:start_config.simulate_multicast, - } - } -} - -#[cfg(windows)] -fn subnet_mask_to_integer(subnet_mask: u32) -> u8 { - let mut mask_bits = subnet_mask; - let mut num_bits = 0; - while mask_bits != 0 { - num_bits += 1; - mask_bits <<= 1; - } - num_bits as u8 -} - -fn default_false() -> bool { - false -} - -fn default_version() -> String { - "1.0.6".to_string() -} - -fn default_str() -> String { - "".to_string() -} - -fn default_vec() -> Vec { - vec![] -} - -fn default_pid() -> u32 { - 0 -} - -// impl ArgsConfig { -// pub fn new(tap: bool, token: String, name: String, server: SocketAddr, -// nat_test_server: &Vec, device_id: String, -// in_ips: Vec<(u32, u32, Ipv4Addr)>, out_ips: Vec<(u32, u32, Ipv4Addr)>, ) -> Self { -// -// Self { -// tap, -// version: "1.0".to_string(), -// token, -// name, -// command_port: None, -// server: server.to_string(), -// nat_test_server: nat_test_server.iter().map(|v| v.to_string()).collect::>(), -// device_id, -// pid: 0, -// } -// } -// } - -pub fn lock_file() -> io::Result { - let path = get_home().join(".lock"); - let file = File::create(path)?; - file.sync_all()?; - Ok(file) -} - - -fn save_runtime_data(config: RuntimeData) -> io::Result<()> { - let config_path = get_runtime_data_path(); - let str = serde_yaml::to_string(&config).unwrap(); - let mut file = File::create(config_path)?; - file.write_all(str.as_bytes())?; - file.sync_all() -} - -pub fn update_pid(pid: u32) -> io::Result<()> { - let mut config = read_runtime_data()?; - config.pid = pid; - return save_runtime_data(config); -} - -#[cfg(any(unix))] -pub fn read_pid() -> io::Result { - let config = read_runtime_data()?; - Ok(config.pid) -} - -pub fn update_command_port(port: u16) -> io::Result<()> { - let mut config = read_runtime_data()?; - config.command_port = Some(port); - return save_runtime_data(config); -} - -pub fn read_command_port() -> io::Result { - let config = read_runtime_data()?; - if let Some(p) = config.command_port { - Ok(p) - } else { - Err(io::Error::new(io::ErrorKind::Other, "not found config")) - } -} - - pub fn get_home() -> PathBuf { - #[cfg(windows)] - { - if let Some(path) = SWITCH_HOME_PATH.lock().as_ref() { - return path.clone(); - } - } let home = dirs::home_dir().unwrap().join(".switch_desktop"); if !home.exists() { std::fs::create_dir(&home).unwrap(); } home -} - -pub fn get_runtime_data_path() -> PathBuf { - let home = get_home(); - home.join(".data") -} - -fn read_runtime_data() -> io::Result { - let config_path = get_runtime_data_path(); - let mut file = if config_path.exists() { - File::open(config_path)? - } else { - OpenOptions::new().read(true).write(true).truncate(false).create(true).open(config_path)? - }; - let mut str = String::new(); - file.read_to_string(&mut str)?; - match serde_yaml::from_str::(&str) { - Ok(config) => Ok(config), - Err(e) => { - log::warn!("{:?}", e); - Err(io::Error::new(io::ErrorKind::Other, "config error")) - } - } -} - -fn read_config(config_path: PathBuf) -> io::Result { - let mut file = File::open(config_path)?; - let mut str = String::new(); - file.read_to_string(&mut str)?; - match serde_yaml::from_str::(&str) { - Ok(config) => Ok(config), - Err(e) => { - log::warn!("{:?}", e); - Err(io::Error::new(io::ErrorKind::Other, "config error")) - } - } } \ No newline at end of file diff --git a/switch-desktop/src/console_out/mod.rs b/switch-desktop/src/console_out/mod.rs deleted file mode 100644 index 83f55d6..0000000 --- a/switch-desktop/src/console_out/mod.rs +++ /dev/null @@ -1,133 +0,0 @@ -use console::{style, Style}; - -use crate::command::entity::{DeviceItem, RouteItem, Status}; - -pub mod table; - -pub fn console_status(status: Status) { - println!("Name: {}", style(status.name).green()); - println!("Virtual ip: {}", style(status.virtual_ip).green()); - println!("Virtual gateway: {}", style(status.virtual_gateway).green()); - println!("Virtual netmask: {}", style(status.virtual_netmask).green()); - println!("Connection status: {}", style(status.connect_status).green()); - println!("NAT type: {}", style(status.nat_type).green()); - println!("Relay server: {}", style(status.relay_server).green()); - println!("Public ips: {}", style(status.public_ips).green()); - println!("Local ip: {}", style(status.local_ip).green()); -} - -pub fn console_route_table(mut list: Vec) { - if list.is_empty() { - println!("No route found"); - return; - } - list.sort_by(|t1, t2| t1.destination.cmp(&t2.destination)); - let mut out_list = Vec::with_capacity(list.len()); - - out_list.push(vec![("Destination".to_string(), Style::new()), - ("Next Hop".to_string(), Style::new()), - ("Metric".to_string(), Style::new()), - ("Rt".to_string(), Style::new()), - ("Interface".to_string(), Style::new()), ]); - for item in list { - out_list.push(vec![(item.destination, Style::new().green()), - (item.next_hop, Style::new().green()), - (item.metric, Style::new().green()), - (item.rt, Style::new().green()), - (item.interface, Style::new().green())]); - } - - table::println_table(out_list) -} - -pub fn console_device_list(mut list: Vec) { - if list.is_empty() { - println!("No other devices found"); - return; - } - list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); - list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); - let mut out_list = Vec::with_capacity(list.len()); - //表头 - out_list.push(vec![("Name".to_string(), Style::new()), - ("Virtual Ip".to_string(), Style::new()), - ("Status".to_string(), Style::new()), - ("P2P/Relay".to_string(), Style::new()), - ("Rt".to_string(), Style::new())]); - for item in list { - if &item.status == "Online" { - if &item.nat_traversal_type == "p2p" { - out_list.push(vec![(item.name, Style::new().green()), - (item.virtual_ip, Style::new().green()), - (item.status, Style::new().green()), - (item.nat_traversal_type, Style::new().green()), - (item.rt, Style::new().green())]); - } else { - out_list.push(vec![(item.name, Style::new().yellow()), - (item.virtual_ip, Style::new().yellow()), - (item.status, Style::new().yellow()), - (item.nat_traversal_type, Style::new().yellow()), - (item.rt, Style::new().yellow())]); - } - } else { - out_list.push(vec![(item.name, Style::new().color256(102)), - (item.virtual_ip, Style::new().color256(102)), - (item.status, Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102))]); - } - } - table::println_table(out_list) -} - -pub fn console_device_list_all(mut list: Vec) { - if list.is_empty() { - println!("No other devices found"); - return; - } - list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); - list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); - let mut out_list = Vec::with_capacity(list.len()); - //表头 - out_list.push(vec![("Name".to_string(), Style::new()), - ("Virtual Ip".to_string(), Style::new()), - ("Status".to_string(), Style::new()), - ("NAT Type".to_string(), Style::new()), - ("Public Ips".to_string(), Style::new()), - ("Local Ip".to_string(), Style::new()), - ("P2P/Relay".to_string(), Style::new()), - ("Rt".to_string(), Style::new())]); - for item in list { - if &item.status == "Online" { - if &item.nat_traversal_type == "p2p" { - out_list.push(vec![(item.name, Style::new().green()), - (item.virtual_ip, Style::new().green()), - (item.status, Style::new().green()), - (item.nat_traversal_type, Style::new().green()), - (item.rt, Style::new().green()), - (item.nat_type, Style::new().green()), - (item.public_ips, Style::new().green()), - (item.local_ip, Style::new().green())]); - } else { - out_list.push(vec![(item.name, Style::new().yellow()), - (item.virtual_ip, Style::new().yellow()), - (item.status, Style::new().yellow()), - (item.nat_traversal_type, Style::new().yellow()), - (item.rt, Style::new().yellow()), - (item.nat_type, Style::new().yellow()), - (item.public_ips, Style::new().yellow()), - (item.local_ip, Style::new().yellow()), ]); - } - } else { - out_list.push(vec![(item.name, Style::new().color256(102)), - (item.virtual_ip, Style::new().color256(102)), - (item.status, Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), - ("".to_string(), Style::new().color256(102)), ]); - } - } - table::println_table(out_list) -} \ No newline at end of file diff --git a/switch-desktop/src/console_out/table.rs b/switch-desktop/src/console_out/table.rs deleted file mode 100644 index c117b95..0000000 --- a/switch-desktop/src/console_out/table.rs +++ /dev/null @@ -1,23 +0,0 @@ -use console::Style; - -pub fn println_table(table: Vec>) { - if table.is_empty() { - return; - } - let mut width_list = vec![0; table[0].len()]; - for in_list in table.iter() { - for (index, (item, _)) in in_list.iter().enumerate() { - let width = console::measure_text_width(item) + 6; - if width_list[index] < width { - width_list[index] = width; - } - } - } - for in_list in table { - for (col, (item, style)) in in_list.iter().enumerate() { - let str = format!("{:1$}", item, width_list[col]); - print!("{}", style.apply_to(str)); - } - println!() - } -} \ No newline at end of file diff --git a/switch-desktop/src/i18n.rs b/switch-desktop/src/i18n.rs deleted file mode 100644 index 9386b1e..0000000 --- a/switch-desktop/src/i18n.rs +++ /dev/null @@ -1,217 +0,0 @@ -#[cfg(target_os = "windows")] -fn get_default_language() -> Option { - use std::process::Command; - use std::str; - - let output = Command::new("powershell") - .arg("-Command") - .arg("[System.Globalization.CultureInfo]::CurrentCulture.Name") - .output() - .ok()?; - - let language_code = str::from_utf8(&output.stdout) - .ok()? - .trim() - .to_string(); - - Some(language_code) -} - -pub fn init() { - #[cfg(target_os = "windows")] - { - if let Some(l) = get_default_language() { - rust_i18n::set_locale(&l); - } - } -} - -pub fn switch_about() -> String { - rust_i18n::t!("switch_about") -} - -pub fn switch_usage() -> String { - rust_i18n::t!("switch_usage") -} - -pub fn switch_start_about() -> String { - rust_i18n::t!("switch_start_about") -} - -pub fn switch_token_help() -> String { - rust_i18n::t!("switch_token_help") -} - -pub fn switch_name_help() -> String { - rust_i18n::t!("switch_name_help") -} - -pub fn switch_device_id_help() -> String { - rust_i18n::t!("switch_device_id_help") -} - -pub fn switch_server_help() -> String { - rust_i18n::t!("switch_server_help") -} - -pub fn switch_nat_test_server_help() -> String { - rust_i18n::t!("switch_nat_test_server_help") -} - -pub fn switch_log_help() -> String { - rust_i18n::t!("switch_log_help") -} - -pub fn switch_tap_help() -> String { - rust_i18n::t!("switch_tap_help") -} - -pub fn switch_in_ip_help() -> String { - rust_i18n::t!("switch_in_ip_help") -} - -pub fn switch_out_ip_help() -> String { - rust_i18n::t!("switch_out_ip_help") -} -pub fn switch_password_help() -> String { - rust_i18n::t!("switch_password_help") -} -pub fn switch_simulate_multicast_help() -> String { - rust_i18n::t!("switch_simulate_multicast_help") -} - -pub fn switch_config_help() -> String { - rust_i18n::t!("switch_config_help") -} - -pub fn switch_stop_about() -> String { - rust_i18n::t!("switch_stop_about") -} - -pub fn switch_route_about() -> String { - rust_i18n::t!("switch_route_about") -} - -pub fn switch_list_about() -> String { - rust_i18n::t!("switch_list_about") -} - -pub fn switch_list_all_help() -> String { - rust_i18n::t!("switch_list_all_help") -} - -pub fn switch_status_about() -> String { - rust_i18n::t!("switch_status_about") -} - -#[cfg(windows)] -pub fn switch_install_about() -> String { - rust_i18n::t!("switch_install_about") -} - -#[cfg(windows)] -pub fn switch_path_help() -> String { - rust_i18n::t!("switch_path_help") -} - -#[cfg(windows)] -pub fn switch_auto_help() -> String { - rust_i18n::t!("switch_auto_help") -} - -#[cfg(windows)] -pub fn switch_uninstall_about() -> String { - rust_i18n::t!("switch_uninstall_about") -} - -#[cfg(windows)] -pub fn switch_config_about() -> String { - rust_i18n::t!("switch_config_about") -} - -#[cfg(windows)] -pub fn switch_use_root_print() -> String { - rust_i18n::t!("switch_use_admin_print") -} - -#[cfg(unix)] -pub fn switch_use_root_print() -> String { - rust_i18n::t!("switch_use_root_print") -} - -#[cfg(windows)] -pub fn switch_service_not_start_print() -> String { - rust_i18n::t!("switch_service_not_start_print") -} - -pub fn switch_start_successfully_print() -> String { - rust_i18n::t!("switch_start_successfully_print") -} - -#[cfg(windows)] -pub fn switch_start_failed_print() -> String { - rust_i18n::t!("switch_start_failed_print") -} - -#[cfg(windows)] -pub fn switch_service_not_stopped_print() -> String { - rust_i18n::t!("switch_service_not_stopped_print") -} - -#[cfg(windows)] -pub fn switch_server_already_installed_print() -> String { - rust_i18n::t!("switch_server_already_installed_print") -} - -pub fn switch_repeated_start_print() -> String { - rust_i18n::t!("switch_repeated_start_print") -} - -pub fn switch_stopped_print() -> String { - rust_i18n::t!("switch_stopped_print") -} - -pub fn switch_token_not_found_print() -> String { - rust_i18n::t!("switch_token_not_found_print") -} - -pub fn switch_token_cannot_be_empty_print() -> String { - rust_i18n::t!("switch_token_cannot_be_empty_print") -} - -pub fn switch_token_cannot_exceed_64_print() -> String { - rust_i18n::t!("switch_token_cannot_exceed_64_print") -} - -pub fn switch_device_id_is_empty_print() -> String { - rust_i18n::t!("switch_device_id_is_empty_print") -} - -pub fn switch_in_ips_example_print() -> String { - rust_i18n::t!("switch_in_ips_example_print") -} - -pub fn switch_out_ips_example_print() -> String { - rust_i18n::t!("switch_out_ips_example_print") -} - -pub fn switch_relay_server_address_error() -> String { - rust_i18n::t!("switch_relay_server_address_error") -} - -pub fn switch_nat_test_server_address_error() -> String { - rust_i18n::t!("switch_nat_test_server_address_error") -} - -pub fn switch_press_any_key_to_exit() -> String { - rust_i18n::t!("switch_press_any_key_to_exit") -} -pub fn switch_virtual_ip() -> String { - rust_i18n::t!("switch_virtual_ip") -} -pub fn switch_virtual_gateway() -> String { - rust_i18n::t!("switch_virtual_gateway") -} -pub fn switch_please_enter_the_command() -> String { - rust_i18n::t!("switch_please_enter_the_command") -} \ No newline at end of file diff --git a/switch-desktop/src/load_dll/mod.rs b/switch-desktop/src/load_dll/mod.rs new file mode 100644 index 0000000..e1340dc --- /dev/null +++ b/switch-desktop/src/load_dll/mod.rs @@ -0,0 +1,27 @@ +use std::fs::File; +use std::io; +use std::io::Write; +use std::os::windows::ffi::OsStrExt; +use crate::config::get_home; + +const DLL_FILE: &'static [u8] = include_bytes!("../../dll/amd64/wintun.dll"); + +pub fn load_tun_dll() -> io::Result<()> { + let lib_path = get_home().join("lib"); + if !lib_path.exists() { + std::fs::create_dir(&lib_path).unwrap(); + } + let dll_path = lib_path.join("wintun.dll"); + if !dll_path.exists() { + let mut f = File::create(&dll_path)?; + f.write_all(DLL_FILE)?; + f.sync_data()?; + } + let dll_directory = lib_path.as_os_str(); + + let dll_directory_wide: Vec = dll_directory.encode_wide().chain(Some(0)).collect(); + unsafe { + winapi::um::winbase::SetDllDirectoryW(dll_directory_wide.as_ptr()); + } + Ok(()) +} \ No newline at end of file diff --git a/switch-desktop/src/main.rs b/switch-desktop/src/main.rs index 60514d2..ecbda23 100644 --- a/switch-desktop/src/main.rs +++ b/switch-desktop/src/main.rs @@ -1,268 +1,225 @@ -use std::thread; -use std::time::Duration; -use clap::{Parser, Subcommand}; -use console::style; - -use switch::core::Switch; - -use crate::config::log_config::log_init; - -mod command; -mod config; -#[cfg(target_os = "windows")] -mod windows; -#[cfg(any(unix))] -mod unix; -mod console_out; -mod command_args; -mod i18n; - -#[derive(Parser, Debug)] -#[command( -author = "Lu Beilin", -version, -about = "一个虚拟网络工具,启动后会获取一个ip,相同token下的设备之间可以用ip直接通信" -)] -pub struct BaseArgs { - #[clap(subcommand)] - command: Commands, - -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// 启动 - Start(StartArgs), - /// 停止后台服务 - Stop, - /// 安装服务 - /// Install service - #[cfg(target_os = "windows")] - Install(InstallArgs), - /// 卸载服务 - /// Uninstall service - #[cfg(target_os = "windows")] - Uninstall, - /// 配置 - #[cfg(target_os = "windows")] - Config(ConfigArgs), - /// 查看路由 - /// View route - Route, - /// 查看设备列表 - /// View device list - List { - /// 查看所有 - #[arg(short, long)] - all: bool - }, - /// 查看设备当前状态 - /// View the current status of the device - Status, -} - -#[derive(Parser, Debug, Default)] -pub struct StartArgs { - /// 不超过64个字符 - /// 相同token的设备之间才能通信。 - /// 建议使用uuid保证唯一性。 - /// No more than 64 characters - /// Only devices with the same token can communicate with each other. - /// It is recommended to use uuid to ensure uniqueness - #[arg(long)] - token: Option, - /// 给设备一个名称,为空时默认用系统版本信息 - /// Give the device a name. If it is blank, the system version information will be used by default - #[arg(long, action)] - name: Option, - /// 设备唯一标识,为空时默认使用MAC地址,不超过64个字符 - /// Unique identification of the device. If it is blank, the MAC address is used by default. No more than 64 characters - #[arg(long)] - device_id: Option, - /// 注册和中继服务器地址 - /// Register and relay server address - #[arg(long)] - server: Option, - /// NAT检测服务地址,使用逗号分隔 - /// NAT detection service address. Use comma to separate - #[arg(long)] - nat_test_server: Option, - /// 关闭命令服务,关闭后不能在其他进程直接使用route、list等命令查看信息 - /// Turn off the command service. After turning off, you cannot directly use the route, list and other commands to view information in other processes - #[cfg(any(unix))] - #[arg(long)] - off_command_server: bool, - /// 记录日志,输出在 home/.switch_desktop 目录下,长时间使用时不建议开启 - /// Output the log in the "home/.switch_desktop" directory - #[arg(long)] - log: bool, - /// 使用tap网卡 - #[arg(long)] - tap: bool, - /// 配置点对网时使用,--in-ip 192.168.10.0/24,10.26.0.3,表示允许接收网段192.168.10.0/24的数据并转发到10.26.0.3 - /// Use when configuring peer-to-peer networks - #[arg(long)] - in_ip: Option>, - /// 配置点对网时使用,--out-ip 192.168.10.0/24,192.168.1.10,表示允许目标为192.168.10.0/24的数据从网卡192.168.1.10转发出去 - /// Use when configuring peer-to-peer networks - #[arg(long)] - out_ip: Option>, - /// 客户端数据加密 - #[arg(long)] - password:Option, - /// 模拟组播,默认情况下组播数据会被当作广播发送,兼容性更强,但是会造成流量浪费,开启后会模拟真实组播的数据发送 - #[arg(long)] - simulate_multicast:bool, - /// 读取配置文件 --config config_file_path - /// Read configuration file - #[arg(long)] - config: Option, -} - -#[cfg(target_os = "windows")] -#[derive(Parser, Debug)] -pub struct InstallArgs { - /// 安装路径 - /// Service installation path - #[arg(long)] - path: String, - /// 服务开机自启动 - /// Autostart on system startup - #[arg(long)] - auto: bool, -} - -#[cfg(target_os = "windows")] -#[derive(Parser, Debug)] -pub struct ConfigArgs { - /// 服务开机自启动 - /// Autostart on system startup - #[arg(long)] - auto: bool, -} - -#[macro_use] -extern crate rust_i18n; -i18n!("locales", fallback = "en"); +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::net::{Ipv4Addr, ToSocketAddrs}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use common::args_parse::ips_parse; +use switch::core::Config; +use switch::core::{Switch, SwitchUtil}; +use switch::handle::registration_handler::ReqEnum; #[cfg(windows)] +mod load_dll; +#[cfg(windows)] +mod config; + +lazy_static! { + static ref SWITCH:Mutex> = Mutex::new(None); +} + fn main() { - i18n::init(); - let args: Vec<_> = std::env::args().collect(); - if args.len() == 3 && args[1] == windows::SERVICE_FLAG { - //以服务的方式启动 - config::set_win_server_home(std::path::PathBuf::from(&args[2])); - windows::service::start(); - return; - } else { - if !command_args::check() { - return; - } - let args = BaseArgs::parse(); - if let Commands::Start(start_args) = &args.command { - if start_args.log { - let _ = log_init(); + #[cfg(windows)] + { + load_dll::load_tun_dll().unwrap(); + } + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![default_value_name,default_value_device_id,connect,close,list]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +#[tauri::command] +fn default_value_name() -> String { + os_info::get().to_string() +} + +#[tauri::command] +fn default_value_device_id() -> String { + common::identifier::get_unique_identifier().unwrap_or(String::new()) +} + +#[tauri::command] +async fn connect(config: ConnectConfig) -> Result { + let tap = config.tap; + let token = config.token; + let device_id = config.device_id; + let name = config.name; + let server_address_str = config.server_address; + let server_address = match server_address_str.to_socket_addrs() { + Ok(mut addr) => { + if let Some(addr) = addr.next() { + addr + } else { + return Err(String::from("server")); } } - windows::main0(args); - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[tokio::main] -async fn main() { - if sudo::RunningAs::Root != sudo::check() { - println!( - "{}", - style("需要使用root权限执行(Need to execute with root permission)...").red() - ); - sudo::escalate_if_needed().unwrap(); - } - let args = BaseArgs::parse(); - if let Commands::Start(start_args) = &args.command { - if start_args.log { - let _ = log_init(); + Err(e) => { + return Err(format!("server err:{}", e)); } - } - unix::main0(args).await; -} + }; + let nat_test_server = config.nat_test_server.split(" ").flat_map(|a| a.to_socket_addrs()).flatten() + .collect::>(); + let in_ips = config.in_ips.split(" ").into_iter().filter(|e| !e.is_empty()).map(|e| e.to_string()).collect(); + let in_ips = match ips_parse(&in_ips) { + Ok(in_ips) => { in_ips } + Err(e) => { + return Err(format!("inIps err:{}", e)); + } + }; + let out_ips = config.out_ips.split(" ").into_iter().filter(|e| !e.is_empty()).map(|e| e.to_string()).collect(); + let out_ips = match ips_parse(&out_ips) { + Ok(out_ips) => { out_ips } + Err(e) => { + return Err(format!("inIps err:{}", e)); + } + }; + let password = if config.key.is_empty() { + None + } else { + Some(config.key) + }; + let simulate_multicast = config.simulate_multicast; + let config = Config::new(tap, token, device_id, name, server_address, server_address_str, + nat_test_server, in_ips, out_ips, + password, simulate_multicast, None); -pub fn console_listen(switch: &Switch) { - use console::Term; - let term = Term::stdout(); - println!("{}", style(i18n::switch_start_successfully_print()).green()); - let current_device = switch.current_device(); - println!("{}: {:?}", i18n::switch_virtual_ip(), style(current_device.virtual_ip()).green()); - println!("{}: {:?}", i18n::switch_virtual_gateway(), style(current_device.virtual_gateway()).green()); - loop { - println!( - "{}", - style(i18n::switch_please_enter_the_command()).color256(102) - ); - match term.read_line() { - Ok(cmd) => { - #[cfg(unix)] - if cmd.is_empty() { - use libc::{STDIN_FILENO, isatty}; - if !unsafe { isatty(STDIN_FILENO) != 0 } { - return; - } - } - if command(cmd.trim(), &switch).is_err() { - println!("{}", style("stopping").red()); - if let Err(e) = switch.stop() { - println!("stop:{:?}", e); - } - thread::sleep(Duration::from_secs(2)); - break; - } + let mut switch_util = SwitchUtil::new(config).await.unwrap(); + let mut count = 0; + let response = loop { + match switch_util.connect().await { + Ok(response) => { + break response; } Err(e) => { - log::error!("read_line:{:?}", e); - println!("{}", style("stopping...").red()); - if let Err(e) = switch.stop() { - log::error!("stop:{:?}", e); + match e { + ReqEnum::TokenError => { + return Err("token error".to_string()); + } + ReqEnum::AddressExhausted => { + return Err("address exhausted".to_string()); + } + ReqEnum::Timeout => { + count += 1; + if count > 3 { + return Err("connect timeout".to_string()); + } + continue; + } + ReqEnum::ServerError(str) => { + return Err(format!("error:{}", str)); + } + ReqEnum::Other(str) => { + return Err(format!("error:{}", str)); + } } - thread::sleep(Duration::from_secs(1)); - break; + } + } + }; + match switch_util.create_iface() { + Ok(_) => {} + Err(e) => { + return Err(format!("create net interface error:{}", e)); + } + } + match switch_util.build().await { + Ok(switch) => { + let _ = SWITCH.lock().insert(switch); + } + Err(e) => { + return Err(format!("build switch error:{}", e)); + } + } + Ok(ConnectRegResponse { + virtual_ip: response.virtual_ip, + virtual_gateway: response.virtual_gateway, + virtual_netmask: response.virtual_netmask, + }) +} + +#[tauri::command] +fn list() -> Vec { + let mut peer_list = Vec::new(); + let mut guard = SWITCH.lock(); + match &mut *guard { + None => {} + Some(switch) => { + let mut list = switch.device_list(); + let current_device = switch.current_device(); + list.sort_unstable_by_key(|v| { (v.status, v.virtual_ip) }); + for x in list { + let item = if let Some(route) = switch.route(&x.virtual_ip) { + let connect = if route.is_p2p() { + "p2p".to_string() + } else if route.addr == current_device.connect_server { + "server relay".to_string() + } else { + "client relay".to_string() + }; + SwitchPeerItem { + name: x.name, + virtual_ip: x.virtual_ip, + status: format!("{:?}", x.status), + connect, + rt: route.rt.to_string(), + addr: route.addr.to_string(), + } + } else { + SwitchPeerItem { + name: x.name, + virtual_ip: x.virtual_ip, + status: format!("{:?}", x.status), + connect: "".to_string(), + rt: "".to_string(), + addr: "".to_string(), + } + }; + peer_list.push(item); } } } - println!("{}", style("stopped").red()); + peer_list } - -fn command(cmd: &str, switch: &Switch) -> Result<(), ()> { - match cmd { - "route" => { - let list = command::server::command_route(switch); - console_out::console_route_table(list); - } - "list" => { - let list = command::server::command_list(switch); - console_out::console_device_list(list); - } - "status" => { - let status = command::server::command_status(switch); - console_out::console_status(status); - } - "help" | "h" => { - println!("Options: "); - println!( - "{} , Query the virtual IP of other devices", - style("list").green() - ); - println!("{} , View current device status", style("status").green()); - println!("{} , Exit the program", style("exit").green()); - } - "exit" => { - return Err(()); - } - _ => { - println!("command '{}' not found. ", style(cmd).red()); - println!("Try to enter: '{}'", style("help").green()); +#[tauri::command] +async fn close() { + let switch = SWITCH.lock().take(); + match switch { + None => {} + Some(mut switch) => { + switch.stop().unwrap(); + switch.wait_stop().await; } } - Ok(()) +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct ConnectConfig { + pub tap: bool, + pub token: String, + pub device_id: String, + pub name: String, + pub server_address: String, + pub nat_test_server: String, + pub in_ips: String, + pub out_ips: String, + pub key: String, + pub simulate_multicast: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct ConnectRegResponse { + pub virtual_ip: Ipv4Addr, + pub virtual_gateway: Ipv4Addr, + pub virtual_netmask: Ipv4Addr, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct SwitchPeerItem { + pub name: String, + pub virtual_ip: Ipv4Addr, + pub status: String, + pub connect: String, + pub rt: String, + pub addr: String, } diff --git a/switch-desktop/src/unix/mod.rs b/switch-desktop/src/unix/mod.rs deleted file mode 100644 index 5f75f43..0000000 --- a/switch-desktop/src/unix/mod.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::sync::Arc; - -use console::style; -use fs2::FileExt; - -use switch::core::{Config, Switch}; - -use crate::{BaseArgs, Commands, config}; -use crate::command::{command, CommandEnum}; - - -pub async fn main0(base_args: BaseArgs) { - match base_args.command { - Commands::Start(args) => { - let start_config = if let Some(config_path) = &args.config { - match config::read_config_file(config_path.into()) { - Ok(start_config) => { - start_config - } - Err(e) => { - println!("{}", style(&e).red()); - log::error!("{:?}", e); - return; - } - } - } else { - match config::default_config(args) { - Ok(start_config) => { - start_config - } - Err(e) => { - println!("{}", style(&e).red()); - log::error!("{:?}", e); - return; - } - } - }; - let off_command_server = start_config.off_command_server; - let config = Config::new( - start_config.tap, - start_config.token.clone(), - start_config.device_id.clone(), - start_config.name.clone(), - start_config.server, - start_config.nat_test_server.clone(), - start_config.in_ips.clone(), - start_config.out_ips.clone(), - start_config.password.clone(), - start_config.simulate_multicast, - ); - let lock = match config::lock_file() { - Ok(lock) => { - lock - } - Err(e) => { - log::error!("{:?}",e); - println!("文件锁定失败:{:?}", e); - return; - } - }; - if lock.try_lock_exclusive().is_err() { - println!("{}", style("文件被重复打开").red()); - return; - } - let switch = match Switch::start(config).await { - Ok(switch) => { - switch - } - Err(e) => { - log::error!("{:?}", e); - println!("启动switch失败:{:?}", e); - lock.unlock().unwrap(); - return; - } - }; - let switch = Arc::new(switch); - let command_server = crate::command::server::CommandServer::new(); - if off_command_server { - crate::console_listen(&switch); - log::info!("前台任务结束"); - } else { - if let Err(e) = config::update_pid(std::process::id()) { - log::error!("{:?}", e); - } - let switch1 = switch.clone(); - let handle = std::thread::Builder::new().name("cmd-server".into()).spawn(move || { - if let Err(e) = command_server.start(switch1) { - log::error!("{:?}", e); - } - }).unwrap(); - crate::console_listen(&switch); - if let Err(e) = handle.join() { - log::error!("后台任务异常{:?}",e); - } else { - log::info!("后台任务结束"); - } - } - lock.unlock().unwrap(); - } - Commands::Stop => { - command(CommandEnum::Stop); - if let Ok(pid) = config::read_pid() { - if pid != 0 { - let kill_cmd = format!("kill {}", pid); - let kill_out = std::process::Command::new("sh") - .arg("-c") - .arg(&kill_cmd) - .output() - .expect("sh exec error!"); - if !kill_out.status.success() { - println!("cmd:{:?},err:{:?}", kill_cmd, kill_out); - return; - } - } - } - println!("stopped") - } - Commands::Route => { - command(CommandEnum::Route); - } - Commands::List { all } => { - if all { - command(CommandEnum::ListAll); - } else { - command(CommandEnum::List); - } - } - Commands::Status => { - command(CommandEnum::Status); - } - } -} \ No newline at end of file diff --git a/switch-desktop/src/windows/mod.rs b/switch-desktop/src/windows/mod.rs deleted file mode 100644 index 96dcd0e..0000000 --- a/switch-desktop/src/windows/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -use std::{io, thread}; -use std::ffi::OsString; -use std::net::UdpSocket; -use std::path::PathBuf; -use std::time::Duration; - -use console::style; -use fs2::FileExt; -use windows_service::Error; -use windows_service::service::{ - ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState, ServiceType, -}; -use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; - -use switch::core::{Config, Switch}; - -use crate::{BaseArgs, Commands, config, i18n}; -use crate::command::{command, CommandEnum}; - -pub mod service; -mod windows_admin_check; - -pub const SERVICE_FLAG: &'static str = "start_switch_service_v1_"; -pub const SERVICE_NAME: &'static str = "switch-service-v1"; -pub const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; - -fn admin_check() -> bool { - if !windows_admin_check::is_app_elevated() { - println!( - "{}", - style(i18n::switch_use_root_print()).red() - ); - true - } else { - false - } -} - -fn not_started() -> bool { - match service_state() { - Ok(state) => { - if state == ServiceState::Running { - return false; - } else { - println!("{}", i18n::switch_service_not_start_print()) - } - } - Err(e) => { - println!("{:?}", e); - } - } - return true; -} - -pub fn main0(base_args: BaseArgs) { - match base_args.command { - Commands::Start(args) => { - if admin_check() { - return; - } - { - // 允许应用通过防火墙 - let _udp = UdpSocket::bind("0.0.0.0:0").unwrap(); - } - let start_config = if let Some(config_path) = &args.config { - match config::read_config_file(config_path.into()) { - Ok(start_config) => { - start_config - } - Err(e) => { - println!("{}", style(&e).red()); - log::error!("{:?}", e); - return; - } - } - } else { - match config::default_config(args) { - Ok(start_config) => { - start_config - } - Err(e) => { - println!("{}", style(&e).red()); - log::error!("{:?}", e); - return; - } - } - }; - match service_state() { - Ok(state) => { - if state == ServiceState::Stopped { - match start() { - Ok(_) => { - //需要检查启动状态 - thread::sleep(Duration::from_secs(2)); - println!("{}", style(i18n::switch_start_successfully_print()).green()); - } - Err(e) => { - log::error!("{:?}", e); - println!("{}:{}", style(i18n::switch_start_failed_print()).red(), e); - } - } - } else { - println!("{}", i18n::switch_service_not_stopped_print()); - } - } - Err(e) => { - match e { - Error::Winapi(ref e) => { - if let Some(code) = e.raw_os_error() { - if code == 1060 { - //指定的服务未安装。 - let config = Config::new( - start_config.tap, - start_config.token, - start_config.device_id, - start_config.name, - start_config.server, - start_config.nat_test_server, - start_config.in_ips, - start_config.out_ips, - start_config.password, - start_config.simulate_multicast, - ); - let lock = match config::lock_file() { - Ok(lock) => { - lock - } - Err(e) => { - log::error!("文件锁定失败:{:?}",e); - println!("文件锁定失败:{:?}", e); - return; - } - }; - if lock.try_lock_exclusive().is_err() { - println!("{}", style(i18n::switch_repeated_start_print()).red()); - return; - } - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async move { - match Switch::start(config).await { - Ok(switch) => { - crate::console_listen(&switch); - } - Err(e) => { - log::error!("{:?}", e); - println!("启动switch失败:{:?}", e); - } - } - }); - lock.unlock().unwrap(); - return; - } - } - } - _ => {} - } - println!("{:?}", e); - } - }; - pause(); - } - Commands::Stop => { - if not_started() { - return; - } - if admin_check() { - return; - } - match stop() { - Ok(_) => { - println!("{}", style(i18n::switch_stopped_print()).green()) - } - Err(e) => { - log::error!("{:?}", e); - println!("停止失败:{}", e); - } - } - pause(); - } - Commands::Install(args) => { - if admin_check() { - return; - } - if service_state().is_ok() { - println!("{}", i18n::switch_server_already_installed_print()); - return; - } - let path: PathBuf = args.path.into(); - if !path.exists() { - std::fs::create_dir_all(&path).unwrap(); - } - if !path.is_dir() { - println!("参数必须为文件目录(Parameter must be a file directory)"); - } else { - if let Err(e) = install(path, args.auto) { - log::error!("{:?}", e); - println!("安装失败:{}", e); - } else { - println!("{}", style("安装成功(Installation succeeded)").green()) - } - } - pause(); - } - Commands::Uninstall => { - if admin_check() { - return; - } - if service_state().is_err() { - println!("服务未安装"); - } - if let Err(e) = uninstall() { - log::error!("{:?}", e); - println!("卸载失败:{}", e); - } else { - println!("{}", style("卸载成功(Uninstall succeeded)").green()) - } - pause(); - } - Commands::Config(args) => { - if service_state().is_err() { - println!("服务未安装"); - } - if let Err(e) = change(args.auto) { - log::error!("{:?}", e); - println!("配置失败:{}", e); - } else { - println!("{}", style("配置成功(Config succeeded)").green()) - } - pause(); - } - Commands::Route => { - if not_started() { - return; - } - command(CommandEnum::Route); - } - Commands::List { all } => { - if not_started() { - return; - } - if all { - command(CommandEnum::ListAll); - } else { - command(CommandEnum::List); - } - } - Commands::Status => { - if not_started() { - return; - } - command(CommandEnum::Status); - } - } -} - -fn pause() { - println!( - "{}", - style(i18n::switch_press_any_key_to_exit()).green() - ); - use console::Term; - let term = Term::stdout(); - let _ = term.read_char().unwrap(); -} - -fn install(mut path: PathBuf, auto: bool) -> Result<(), Error> { - if !path.is_absolute() { - path = path.canonicalize().unwrap(); - } - let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - let current_exe_path = std::env::current_exe().unwrap(); - let service_path = path.join("switch-service-v1.exe"); - std::fs::copy(current_exe_path, service_path.as_path()).unwrap(); - if let Err(e) = std::fs::copy("wintun.dll", path.join("wintun.dll").as_path()) { - if e.kind() == io::ErrorKind::NotFound { - println!("'wintun.dll' not found. Please put 'wintun.dll' in the current directory"); - std::process::exit(0); - } else { - panic!("{:?}", e) - } - } - let mut launch_arguments = Vec::new(); - launch_arguments.push(OsString::from(SERVICE_FLAG)); - launch_arguments.push(OsString::from( - config::get_home().to_str().unwrap(), - )); - let start_type = if auto { - ServiceStartType::AutoStart - } else { - ServiceStartType::OnDemand - }; - let service_info = ServiceInfo { - name: OsString::from(SERVICE_NAME), - display_name: OsString::from("switch service v1"), - service_type: SERVICE_TYPE, - start_type, - error_control: ServiceErrorControl::Normal, - executable_path: service_path.into(), - launch_arguments, - dependencies: vec![], - account_name: None, // run as System - account_password: None, - }; - let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?; - service.set_description("A VPN")?; - Ok(()) -} - -fn change(auto: bool) -> Result<(), Error> { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - - let service_access = ServiceAccess::QUERY_CONFIG | ServiceAccess::CHANGE_CONFIG; - let service = service_manager.open_service(SERVICE_NAME, service_access)?; - let config = service.query_config()?; - let start_type = if auto { - ServiceStartType::AutoStart - } else { - ServiceStartType::OnDemand - }; - let executable_path = config.executable_path.to_string_lossy().to_string(); - let executable_path = if executable_path.starts_with('"') && executable_path.ends_with('"') { - &executable_path[1..executable_path.len() - 1] - } else { - &executable_path - }; - let mut split = executable_path.split(SERVICE_FLAG); - let executable_path = split.next().unwrap().trim(); - let executable_path = if executable_path.starts_with('"') && executable_path.ends_with('"') { - PathBuf::from(&executable_path[1..executable_path.len() - 1]) - } else { - PathBuf::from(executable_path) - }; - let home_path = split.next().unwrap().trim(); - let launch_arguments = vec![OsString::from(SERVICE_FLAG), OsString::from(home_path)]; - let service_info = ServiceInfo { - name: OsString::from(SERVICE_NAME), - display_name: config.display_name, - service_type: SERVICE_TYPE, - start_type, - error_control: config.error_control, - executable_path, - launch_arguments, - dependencies: config.dependencies, - account_name: None, // run as System - account_password: None, - }; - service.change_config(&service_info)?; - Ok(()) -} - -fn uninstall() -> Result<(), Error> { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - - let service_access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE; - let service = service_manager.open_service(SERVICE_NAME, service_access)?; - - let service_status = service.query_status()?; - if service_status.current_state != ServiceState::Stopped { - service.stop()?; - // Wait for service to stop - thread::sleep(Duration::from_secs(1)); - } - service.delete()?; - Ok(()) -} - -fn start() -> Result<(), Error> { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - let service = service_manager.open_service(SERVICE_NAME, ServiceAccess::START)?; - let args: Vec<_> = std::env::args().collect(); - service.start(&args[1..]) -} - -fn service_state() -> Result { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - - let service_access = ServiceAccess::QUERY_STATUS; - let service = service_manager.open_service(SERVICE_NAME, service_access)?; - let service_status = service.query_status()?; - return Ok(service_status.current_state); -} - -fn stop() -> Result<(), Error> { - let manager_access = ServiceManagerAccess::CONNECT; - let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?; - let service = service_manager.open_service(SERVICE_NAME, ServiceAccess::STOP)?; - service.stop()?; - Ok(()) -} diff --git a/switch-desktop/src/windows/service.rs b/switch-desktop/src/windows/service.rs deleted file mode 100644 index 24de78e..0000000 --- a/switch-desktop/src/windows/service.rs +++ /dev/null @@ -1,202 +0,0 @@ -// #[macro_use] -// extern crate windows_service; - -use std::ffi::OsString; -use std::sync::Arc; -use std::io; -use std::io::Write; -use std::path::PathBuf; -use std::time::Duration; -use clap::Parser; - -use windows_service::{define_windows_service, service_control_handler, service_dispatcher}; -use windows_service::service::{ - ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, -}; -use windows_service::service_control_handler::ServiceControlHandlerResult; - -use switch::core::{Config, Switch}; - -use crate::{BaseArgs, Commands, config}; -use crate::windows::SERVICE_NAME; - -define_windows_service!(ffi_service_main, switch_service_main); -pub fn switch_service_main(arguments: Vec) { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - match service_main(arguments).await { - Ok(_) => {} - Err(e) => { - log::error!("启动服务失败:{:?}",e); - } - } - }) -} - -async fn service_main(arguments: Vec) -> windows_service::Result<()> { - let parker = crossbeam::sync::Parker::new(); - let un_parker = parker.unparker().clone(); - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - // Notifies a service to report its current status information to the service - // control manager. Always return NoError even if not implemented. - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - - // Handle stop - ServiceControl::Stop => { - un_parker.unpark(); - log::info!("handler 服务停止"); - ServiceControlHandlerResult::NoError - } - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - - // Register system service event handler. - // The returned status handle should be used to report service status changes to the system. - let status_handle = - service_control_handler::register(SERVICE_NAME, event_handler)?; - - // Tell the system that service is running - status_handle.set_service_status(ServiceStatus { - service_type: crate::windows::SERVICE_TYPE, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - })?; - match start_switch(arguments).await { - Ok(_) => { - parker.park(); - } - Err(e) => { - log::error!("服务启动失败 {:?}",e); - } - } - status_handle.set_service_status(ServiceStatus { - service_type: crate::windows::SERVICE_TYPE, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }) -} - -fn auto_config_path() -> io::Result { - Ok(config::get_win_server_home().join("auto_config.yaml")) -} - -fn save_auto_config(start_config: config::StartConfig) -> io::Result<()> { - let mut file = std::fs::File::create(auto_config_path()?)?; - let config = config::ArgsConfig::new(start_config); - match serde_yaml::to_string(&config) { - Ok(yaml) => { - file.write_all(yaml.as_bytes()) - } - Err(e) => { - Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", e))) - } - } -} - -async fn start_switch(arguments: Vec) -> switch::Result<()> { - let start_config = match BaseArgs::try_parse_from(arguments) { - Ok(args) => { - match args.command { - Commands::Start(args) => { - if args.log { - let _ = config::log_config::log_service_init(); - } - if let Some(config_path) = &args.config { - match config::read_config_file(config_path.into()) { - Ok(start_config) => { - if let Err(e) = save_auto_config(start_config.clone()) { - log::warn!("配置文件保存失败:{:?}",e); - } - start_config - } - Err(e) => { - log::error!("{:?}", e); - return Err(switch::error::Error::Stop(e)); - } - } - } else { - match config::default_config(args) { - Ok(start_config) => { - if let Err(e) = save_auto_config(start_config.clone()) { - log::warn!("配置文件保存失败:{:?}",e); - } - start_config - } - Err(e) => { - log::error!("{:?}", e); - return Err(switch::error::Error::Stop(e)); - } - } - } - } - _ => { - return Err(switch::error::Error::Stop("配置文件错误".to_string())); - } - } - } - Err(_) => { - match config::read_config_file(auto_config_path()?) { - Ok(start_config) => { - if start_config.log { - let _ = config::log_config::log_service_init(); - } - start_config - } - Err(e) => { - return Err(switch::error::Error::Stop(e)); - } - } - } - }; - let config = Config::new( - start_config.tap, - start_config.token, - start_config.device_id, - start_config.name, - start_config.server, - start_config.nat_test_server, - start_config.in_ips, - start_config.out_ips, - start_config.password, - start_config.simulate_multicast, - ); - log::info!("switch-service服务启动"); - - - tokio::spawn(async move { - match Switch::start(config).await { - Ok(switch) => { - let switch = Arc::new(switch); - let command_server = crate::command::server::CommandServer::new(); - if let Err(e) = config::update_pid(std::process::id()) { - log::error!("{:?}", e); - } - if let Err(e) = command_server.start(switch) { - log::error!("{:?}", e); - } - } - Err(e) => { - log::error!("{:?}", e); - } - }; - }); - Ok(()) -} - -pub fn start() { - log::info!("以服务的方式启动"); - service_dispatcher::start(SERVICE_NAME, ffi_service_main).unwrap(); -} diff --git a/switch-desktop/src/windows/windows_admin_check.rs b/switch-desktop/src/windows/windows_admin_check.rs deleted file mode 100644 index c3531bf..0000000 --- a/switch-desktop/src/windows/windows_admin_check.rs +++ /dev/null @@ -1,76 +0,0 @@ -/// 使用 https://github.com/spa5k/is_sudo/blob/main/src/window.rs -use std::io::Error; -use std::ptr; - -use winapi::um::handleapi::CloseHandle; -use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken}; -use winapi::um::securitybaseapi::GetTokenInformation; -use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY}; - -// Use std::io::Error::last_os_error for errors. -// NOTE: For this example I'm simple passing on the OS error. -// However, customising the error could provide more context - -/// Returns true if the current process has admin rights, otherwise false. -pub fn is_app_elevated() -> bool { - _is_app_elevated().unwrap_or(false) -} - -/// On success returns a bool indicating if the current process has admin rights. -/// Otherwise returns an OS error. -/// -/// This is unlikely to fail but if it does it's even more unlikely that you have admin permissions anyway. -/// Therefore the public function above simply eats the error and returns a bool. -fn _is_app_elevated() -> Result { - let token = QueryAccessToken::from_current_process()?; - token.is_elevated() -} - -/// A safe wrapper around querying Windows access tokens. -pub struct QueryAccessToken(HANDLE); - -impl QueryAccessToken { - pub fn from_current_process() -> Result { - unsafe { - let mut handle: HANDLE = ptr::null_mut(); - let result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut handle); - - if result != 0 { - Ok(Self(handle)) - } else { - Err(Error::last_os_error()) - } - } - } - - /// On success returns a bool indicating if the access token has elevated privilidges. - /// Otherwise returns an OS error. - pub fn is_elevated(&self) -> Result { - unsafe { - let mut elevation = TOKEN_ELEVATION::default(); - let size = std::mem::size_of::() as u32; - let mut ret_size = size; - // The weird looking repetition of `as *mut _` is casting the reference to a c_void pointer. - if GetTokenInformation( - self.0, - TokenElevation, - &mut elevation as *mut _ as *mut _, - size, - &mut ret_size, - ) != 0 - { - Ok(elevation.TokenIsElevated != 0) - } else { - Err(Error::last_os_error()) - } - } - } -} - -impl Drop for QueryAccessToken { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { CloseHandle(self.0) }; - } - } -} diff --git a/switch-desktop/tauri.conf.json b/switch-desktop/tauri.conf.json new file mode 100644 index 0000000..a419846 --- /dev/null +++ b/switch-desktop/tauri.conf.json @@ -0,0 +1,69 @@ +{ + "build": { + "withGlobalTauri": true, + "beforeBuildCommand": "", + "beforeDevCommand": "", + "devPath": "./ui", + "distDir": "./ui" + }, + "package": { + "productName": "switch-desktop", + "version": "1.1.0" + }, + "tauri": { + "allowlist": { + "all": false + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": [], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "top.wherewego.switch", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "height": 600, + "resizable": true, + "title": "Switch Desktop", + "width": 892, + "minWidth": 892, + "minHeight": 600, + "center": true + } + ] + } +} diff --git a/switch-desktop/ui/index.html b/switch-desktop/ui/index.html new file mode 100644 index 0000000..341702e --- /dev/null +++ b/switch-desktop/ui/index.html @@ -0,0 +1,681 @@ + + + + + + Switch Desktop + + + + + +
+
+
+
+
+
+
+
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+
+ close +
+
+
+ +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/switch-desktop/ui/js/jquery-3.7.0.min.js b/switch-desktop/ui/js/jquery-3.7.0.min.js new file mode 100644 index 0000000..e7e29d5 --- /dev/null +++ b/switch-desktop/ui/js/jquery-3.7.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.0",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&z(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function X(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function z(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Me(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0