366 lines
9.5 KiB
Rust
366 lines
9.5 KiB
Rust
use percent_encoding::percent_decode_str;
|
|
use serde::{Deserialize, Serialize};
|
|
use url::Url;
|
|
|
|
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1";
|
|
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080;
|
|
pub const DEFAULT_LOCAL_SINGBOX_SERVICE_NAME: &str = "ProxyWardenSingBox";
|
|
pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str =
|
|
r"C:\Program Files\ProxyWarden\components\sing-box";
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
pub enum Protocol {
|
|
Tcp,
|
|
Udp,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ProfileItemType {
|
|
Process,
|
|
Folder,
|
|
Exe,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum TargetKind {
|
|
Local,
|
|
External,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ProxyProtocol {
|
|
Socks5,
|
|
Http,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum ComponentId {
|
|
ControlApp,
|
|
Proxyfier,
|
|
Singbox,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ComponentState {
|
|
Installed,
|
|
Missing,
|
|
Stopped,
|
|
Running,
|
|
Error,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProfileItemInput {
|
|
#[serde(rename = "type")]
|
|
pub item_type: String,
|
|
pub value: String,
|
|
#[serde(default)]
|
|
pub recursive: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProfileInput {
|
|
pub id: Option<String>,
|
|
pub name: String,
|
|
#[serde(default = "default_enabled")]
|
|
pub enabled: bool,
|
|
#[serde(default = "default_target_id")]
|
|
pub target_id: String,
|
|
#[serde(default = "default_protocols")]
|
|
pub protocols: Vec<String>,
|
|
#[serde(default)]
|
|
pub items: Vec<ProfileItemInput>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ProfileItem {
|
|
#[serde(rename = "type")]
|
|
pub item_type: ProfileItemType,
|
|
pub value: String,
|
|
pub recursive: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Profile {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub enabled: bool,
|
|
pub target_id: String,
|
|
pub protocols: Vec<Protocol>,
|
|
pub items: Vec<ProfileItem>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct TargetInput {
|
|
pub id: Option<String>,
|
|
pub name: String,
|
|
#[serde(default = "default_target_kind")]
|
|
pub kind: String,
|
|
#[serde(default = "default_proxy_protocol")]
|
|
pub protocol: String,
|
|
pub host: String,
|
|
pub port: u32,
|
|
#[serde(default)]
|
|
pub requires_component: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Target {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub kind: TargetKind,
|
|
pub protocol: ProxyProtocol,
|
|
pub host: String,
|
|
pub port: u16,
|
|
pub requires_component: Option<ComponentId>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ComponentStatus {
|
|
pub id: ComponentId,
|
|
pub name: String,
|
|
pub state: ComponentState,
|
|
pub installed: bool,
|
|
pub running: bool,
|
|
pub version: Option<String>,
|
|
pub path: Option<String>,
|
|
#[serde(default)]
|
|
pub service_name: Option<String>,
|
|
#[serde(default)]
|
|
pub service_status: Option<String>,
|
|
#[serde(default)]
|
|
pub problems: Vec<String>,
|
|
#[serde(default)]
|
|
pub actions: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct LocalSingBoxConfig {
|
|
#[serde(default)]
|
|
pub subscription_url: Option<String>,
|
|
#[serde(default)]
|
|
pub device_hwid: Option<String>,
|
|
#[serde(default)]
|
|
pub selected_server_tag: Option<String>,
|
|
#[serde(default)]
|
|
pub selected_server_id: Option<String>,
|
|
#[serde(default = "default_local_singbox_listen_host")]
|
|
pub listen_host: String,
|
|
#[serde(default = "default_local_singbox_listen_port")]
|
|
pub listen_port: u16,
|
|
#[serde(default = "default_local_singbox_service_name")]
|
|
pub service_name: String,
|
|
#[serde(default = "default_local_singbox_install_root", skip_serializing)]
|
|
pub install_root: String,
|
|
#[serde(default)]
|
|
pub updated_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum StorageMigrationOutcome {
|
|
InitializedEmpty,
|
|
AdoptedWithoutLegacyImport,
|
|
ImportedLegacyConfig,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct StorageMeta {
|
|
pub storage_schema_version: u32,
|
|
pub outcome: StorageMigrationOutcome,
|
|
pub migration_id: String,
|
|
pub completed_at_epoch_seconds: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ComponentLayoutMeta {
|
|
pub component_layout_version: u32,
|
|
pub verified_at_epoch_seconds: u64,
|
|
}
|
|
|
|
impl LocalSingBoxConfig {
|
|
pub fn subscription_display_url(&self) -> Option<String> {
|
|
self.subscription_url
|
|
.as_deref()
|
|
.map(redact_subscription_url)
|
|
}
|
|
|
|
pub fn normalize_percent_encoded_tags(&mut self) {
|
|
if let Some(selected_server_tag) = self.selected_server_tag.as_mut() {
|
|
*selected_server_tag = decode_percent_encoded_utf8(selected_server_tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for LocalSingBoxConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
subscription_url: None,
|
|
device_hwid: None,
|
|
selected_server_tag: None,
|
|
selected_server_id: None,
|
|
listen_host: default_local_singbox_listen_host(),
|
|
listen_port: default_local_singbox_listen_port(),
|
|
service_name: default_local_singbox_service_name(),
|
|
install_root: default_local_singbox_install_root(),
|
|
updated_at: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SubscriptionCache {
|
|
pub config: serde_json::Value,
|
|
#[serde(default)]
|
|
pub servers: Vec<SubscriptionServer>,
|
|
#[serde(default)]
|
|
pub user_info: serde_json::Map<String, serde_json::Value>,
|
|
pub fetched_at: String,
|
|
}
|
|
|
|
impl SubscriptionCache {
|
|
pub fn normalize_percent_encoded_tags(&mut self) {
|
|
for server in &mut self.servers {
|
|
server.tag = decode_percent_encoded_utf8(&server.tag);
|
|
server.ensure_id();
|
|
}
|
|
|
|
// Outbound bytes define stable identity. Decode display labels only.
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SubscriptionServer {
|
|
#[serde(default)]
|
|
pub id: String,
|
|
pub tag: String,
|
|
#[serde(rename = "type")]
|
|
pub server_type: String,
|
|
pub server: String,
|
|
pub server_port: u16,
|
|
}
|
|
|
|
impl SubscriptionServer {
|
|
pub fn ensure_id(&mut self) {
|
|
if self.id.trim().is_empty() {
|
|
self.id = subscription_server_id(
|
|
&self.server_type,
|
|
&self.tag,
|
|
&self.server,
|
|
self.server_port,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn subscription_server_id(
|
|
server_type: &str,
|
|
tag: &str,
|
|
server: &str,
|
|
server_port: u16,
|
|
) -> String {
|
|
format!(
|
|
"{}|{}|{}|{}",
|
|
server_type.trim().to_ascii_lowercase(),
|
|
tag.trim(),
|
|
server.trim().to_ascii_lowercase(),
|
|
server_port
|
|
)
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ActivityEntry {
|
|
pub id: String,
|
|
pub at: String,
|
|
pub level: ActivityLevel,
|
|
pub title: String,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ActivityLevel {
|
|
Info,
|
|
Warning,
|
|
Error,
|
|
Success,
|
|
}
|
|
|
|
fn default_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_target_id() -> String {
|
|
"local-singbox".to_string()
|
|
}
|
|
|
|
fn default_protocols() -> Vec<String> {
|
|
vec!["TCP".to_string(), "UDP".to_string()]
|
|
}
|
|
|
|
fn default_target_kind() -> String {
|
|
"external".to_string()
|
|
}
|
|
|
|
fn default_proxy_protocol() -> String {
|
|
"socks5".to_string()
|
|
}
|
|
|
|
fn default_local_singbox_listen_host() -> String {
|
|
DEFAULT_LOCAL_SINGBOX_LISTEN_HOST.to_string()
|
|
}
|
|
|
|
fn default_local_singbox_listen_port() -> u16 {
|
|
DEFAULT_LOCAL_SINGBOX_LISTEN_PORT
|
|
}
|
|
|
|
fn default_local_singbox_service_name() -> String {
|
|
DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string()
|
|
}
|
|
|
|
fn default_local_singbox_install_root() -> String {
|
|
DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string()
|
|
}
|
|
|
|
pub fn redact_subscription_url(raw_url: &str) -> String {
|
|
let trimmed = raw_url.trim();
|
|
if trimmed.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let Ok(parsed) = Url::parse(trimmed) else {
|
|
return "***".to_string();
|
|
};
|
|
|
|
let host = parsed.host_str().unwrap_or("subscription");
|
|
let host = if host.contains(':') && !host.starts_with('[') {
|
|
format!("[{host}]")
|
|
} else {
|
|
host.to_string()
|
|
};
|
|
let port = parsed
|
|
.port()
|
|
.map(|port| format!(":{port}"))
|
|
.unwrap_or_default();
|
|
|
|
format!("{}://{}{}/...", parsed.scheme(), host, port)
|
|
}
|
|
|
|
pub fn decode_percent_encoded_utf8(value: &str) -> String {
|
|
percent_decode_str(value)
|
|
.decode_utf8()
|
|
.map(|decoded| decoded.into_owned())
|
|
.unwrap_or_else(|_| value.to_string())
|
|
}
|