diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 64e787892..f11f1521f 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -65,7 +65,7 @@ pub async fn enhance_profiles() -> CmdResult { /// 导入配置文件 #[tauri::command] pub async fn import_profile(url: std::string::String, option: Option) -> CmdResult { - logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url); + logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", help::mask_url(&url)); // 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹 let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await { @@ -107,7 +107,7 @@ pub async fn import_profile(url: std::string::String, option: Option) handle::Handle::notify_profile_changed(uid); } - logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url); + logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", help::mask_url(&url)); AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index e8e974afe..8be7b6d65 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -2,6 +2,7 @@ use crate::{ cmd, config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe}, core::{CoreManager, handle, tray}, + utils::help::mask_url, }; use anyhow::{Result, bail}; use clash_verge_logging::{Type, logging, logging_error}; @@ -83,9 +84,11 @@ async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result Type::Config, "[订阅更新] {} 是远程订阅,URL: {}", uid, - item.url - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? + mask_url( + item.url + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Profile URL is None"))? + ) ); Ok(Some(( item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?, diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index c0d2e18c8..4c71778a6 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -98,6 +98,57 @@ pub fn parse_str(target: &str, key: &str) -> Option { }) } +/// Mask sensitive parts of a subscription URL for safe logging. +/// Examples: +/// - `https://example.com/api/v1/clash?token=abc123` → `https://example.com/api/v1/clash?token=***` +/// - `https://example.com/abc123def456ghi789/clash` → `https://example.com/***/clash` +pub fn mask_url(url: &str) -> String { + // Split off query string + let (path_part, query_part) = match url.find('?') { + Some(pos) => (&url[..pos], Some(&url[pos + 1..])), + None => (url, None), + }; + + // Extract scheme+host prefix (everything up to the first '/' after "://") + let host_end = path_part + .find("://") + .and_then(|scheme_end| { + path_part[scheme_end + 3..] + .find('/') + .map(|slash| scheme_end + 3 + slash) + }) + .unwrap_or(path_part.len()); + + let scheme_and_host = &path_part[..host_end]; + let path = &path_part[host_end..]; // starts with '/' or empty + + let mut result = scheme_and_host.to_owned(); + + // Mask path segments that look like tokens (longer than 16 chars) + if !path.is_empty() { + let masked: Vec<&str> = path + .split('/') + .map(|seg| if seg.len() > 16 { "***" } else { seg }) + .collect(); + result.push_str(&masked.join("/")); + } + + // Keep query param keys, mask values + if let Some(query) = query_part { + result.push('?'); + let masked_query: Vec = query + .split('&') + .map(|param| match param.find('=') { + Some(eq) => format!("{}=***", ¶m[..eq]), + None => param.to_owned(), + }) + .collect(); + result.push_str(&masked_query.join("&")); + } + + result +} + /// get the last part of the url, if not found, return empty string pub fn get_last_part_and_decode(url: &str) -> Option { let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part