From c599f5547ec98cf998626a30c0003b41a5bd5579 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:17:57 +0800 Subject: [PATCH] fix(proxy): validate user-entered URLs via isValidUrl Delay test inputs now reject non-http(s) URLs with a warning toast and fall back to the default URL instead of erroring every node in the core. Subscription import and profile card host parsing reuse the same validator, fixing bare-domain URLs shown as local files and credential-bearing URLs leaking into the card. --- src/components/home/home-profile-card.tsx | 3 ++- src/components/profile/profile-item.tsx | 5 ++--- src/components/proxy/proxy-group-tools.tsx | 6 ++++++ src/components/proxy/proxy-head.tsx | 6 ++++++ src/locales/ar/proxies.json | 3 +++ src/locales/de/proxies.json | 3 +++ src/locales/en/proxies.json | 3 +++ src/locales/es/proxies.json | 3 +++ src/locales/fa/proxies.json | 3 +++ src/locales/id/proxies.json | 3 +++ src/locales/jp/proxies.json | 3 +++ src/locales/ko/proxies.json | 3 +++ src/locales/ru/proxies.json | 3 +++ src/locales/tr/proxies.json | 3 +++ src/locales/tt/proxies.json | 3 +++ src/locales/zh/proxies.json | 3 +++ src/locales/zhtw/proxies.json | 3 +++ src/pages/profiles.tsx | 3 ++- src/services/delay.ts | 6 ++++++ src/types/generated/i18n-keys.ts | 1 + src/types/generated/i18n-resources.ts | 3 +++ 21 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 7ac4203da..5b196fb4c 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -27,6 +27,7 @@ import { useNavigate } from 'react-router' import { useAppRefreshers } from '@/providers/app-data-context' import { updateProfile } from '@/services/cmds' import { showNotice } from '@/services/notice-service' +import { isValidUrl } from '@/utils/network' import { openExternalUrl } from '@/utils/open-external-url' import parseTraffic from '@/utils/parse-traffic' @@ -41,7 +42,7 @@ const round = keyframes` // 辅助函数解析URL和过期时间 const parseUrl = (url?: string) => { if (!url) return '-' - if (url.startsWith('http')) return new URL(url).host + if (isValidUrl(url)) return new URL(url).host return 'local' } diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index bfa557b14..61f938f91 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -43,6 +43,7 @@ import { showNotice } from '@/services/notice-service' import { useLoadingCache, useSetLoadingCache } from '@/services/states' import type { TranslationKey } from '@/types/generated/i18n-keys' import { debugLog } from '@/utils/debug' +import { isValidUrl } from '@/utils/network' import { openExternalUrl } from '@/utils/open-external-url' import parseTraffic from '@/utils/parse-traffic' @@ -965,9 +966,7 @@ export const ProfileItem = memo(ProfileItemBase) function parseUrl(url?: string) { if (!url) return '' - const regex = /https?:\/\/(.+?)\// - const result = url.match(regex) - return result ? result[1] : 'local file' + return isValidUrl(url) ? new URL(url).host : 'local file' } function parseExpire(expire?: number) { diff --git a/src/components/proxy/proxy-group-tools.tsx b/src/components/proxy/proxy-group-tools.tsx index 903664de5..44cc8a3da 100644 --- a/src/components/proxy/proxy-group-tools.tsx +++ b/src/components/proxy/proxy-group-tools.tsx @@ -17,6 +17,8 @@ import { useTranslation } from 'react-i18next' import { useVerge } from '@/hooks/use-verge' import delayManager from '@/services/delay' +import { showNotice } from '@/services/notice-service' +import { isValidUrl } from '@/utils/network' import { BaseSearchBox, type SearchState } from '../base' @@ -174,6 +176,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) { if (testUrl?.trim() && textState !== 'filter') { onHeadState({ textState: 'url' }) } + if (testUrl?.trim() && !isValidUrl(testUrl)) { + showNotice.warning('proxies.feedback.warnings.invalidTestUrl') + return + } onCheckDelay() }} > diff --git a/src/components/proxy/proxy-head.tsx b/src/components/proxy/proxy-head.tsx index 63e0fe5c9..4db54f36f 100644 --- a/src/components/proxy/proxy-head.tsx +++ b/src/components/proxy/proxy-head.tsx @@ -18,7 +18,9 @@ import { useTranslation } from 'react-i18next' import { BaseSearchBox } from '@/components/base' import { useVerge } from '@/hooks/use-verge' import delayManager from '@/services/delay' +import { showNotice } from '@/services/notice-service' import { debugLog } from '@/utils/debug' +import { isValidUrl } from '@/utils/network' import type { ProxySortType } from './use-filter-sort' import type { HeadState } from './use-head-state' @@ -95,6 +97,10 @@ export const ProxyHead = ({ debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`) onHeadState({ textState: 'url' }) } + if (testUrl?.trim() && !isValidUrl(testUrl)) { + showNotice.warning('proxies.feedback.warnings.invalidTestUrl') + return + } onCheckDelay() }} > diff --git a/src/locales/ar/proxies.json b/src/locales/ar/proxies.json index cebbe12a8..55f99cec8 100644 --- a/src/locales/ar/proxies.json +++ b/src/locales/ar/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/de/proxies.json b/src/locales/de/proxies.json index cca58c8a9..cf67c63cd 100644 --- a/src/locales/de/proxies.json +++ b/src/locales/de/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/en/proxies.json b/src/locales/en/proxies.json index f54974713..ead3f0cbf 100644 --- a/src/locales/en/proxies.json +++ b/src/locales/en/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/es/proxies.json b/src/locales/es/proxies.json index ea02c26b1..537790ba3 100644 --- a/src/locales/es/proxies.json +++ b/src/locales/es/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/fa/proxies.json b/src/locales/fa/proxies.json index 3b9185f0f..b430a2475 100644 --- a/src/locales/fa/proxies.json +++ b/src/locales/fa/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/id/proxies.json b/src/locales/id/proxies.json index 869c0768c..7bb44c604 100644 --- a/src/locales/id/proxies.json +++ b/src/locales/id/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/jp/proxies.json b/src/locales/jp/proxies.json index b3f0b17d5..950e65646 100644 --- a/src/locales/jp/proxies.json +++ b/src/locales/jp/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/ko/proxies.json b/src/locales/ko/proxies.json index b534797c5..64e7b6a9c 100644 --- a/src/locales/ko/proxies.json +++ b/src/locales/ko/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} 업데이트 성공", diff --git a/src/locales/ru/proxies.json b/src/locales/ru/proxies.json index fdce686d0..603abacc1 100644 --- a/src/locales/ru/proxies.json +++ b/src/locales/ru/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} успешно обновлён", diff --git a/src/locales/tr/proxies.json b/src/locales/tr/proxies.json index 00ee3b212..7848e5b30 100644 --- a/src/locales/tr/proxies.json +++ b/src/locales/tr/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/tt/proxies.json b/src/locales/tt/proxies.json index 53b3aa9ac..59fcd096b 100644 --- a/src/locales/tt/proxies.json +++ b/src/locales/tt/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/locales/zh/proxies.json b/src/locales/zh/proxies.json index 2713cc1af..cc792f5e7 100644 --- a/src/locales/zh/proxies.json +++ b/src/locales/zh/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} 更新成功", diff --git a/src/locales/zhtw/proxies.json b/src/locales/zhtw/proxies.json index c2e805457..48150df00 100644 --- a/src/locales/zhtw/proxies.json +++ b/src/locales/zhtw/proxies.json @@ -89,6 +89,9 @@ } }, "feedback": { + "warnings": { + "invalidTestUrl": "Test URL must start with http:// or https://" + }, "notifications": { "provider": { "updateSuccess": "{{name}} updated successfully", diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 9da7da3d3..2bd7484d9 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -59,6 +59,7 @@ import { useThemeMode, } from '@/services/states' import { debugLog } from '@/utils/debug' +import { isValidUrl } from '@/utils/network' // 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐 const PROFILE_UPDATE_WORKER_LIMIT = 8 @@ -214,7 +215,7 @@ const ProfilePage = () => { const onImport = async () => { if (!url) return - if (!/^https?:\/\//i.test(url)) { + if (!isValidUrl(url)) { showNotice.error('profiles.page.feedback.errors.invalidUrl') return } diff --git a/src/services/delay.ts b/src/services/delay.ts index 6b188b8f6..18602faaf 100644 --- a/src/services/delay.ts +++ b/src/services/delay.ts @@ -12,6 +12,7 @@ import { } from '@/types/proxy-view' import { debugLog } from '@/utils/debug' import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay' +import { isValidUrl } from '@/utils/network' export type DelaySnapshot = { of: (member: ResolvedProxyMember) => number @@ -152,6 +153,11 @@ class DelayManager { } setUrl(group: string, url: string) { + if (!isValidUrl(url)) { + debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`) + this.urlMap.delete(group) + return + } debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`) this.urlMap.set(group, url) } diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index d2535331c..972d28812 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -324,6 +324,7 @@ export const translationKeys = [ 'proxies.page.messages.directMode', 'proxies.page.title.default', 'proxies.page.title.chainMode', + 'proxies.feedback.warnings.invalidTestUrl', 'proxies.feedback.notifications.provider.updateSuccess', 'proxies.feedback.notifications.provider.updateFailed', 'proxies.feedback.notifications.provider.genericError', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 474314e5b..ad39b14ff 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -500,6 +500,9 @@ export interface TranslationResources { updateSuccess: string } } + warnings: { + invalidTestUrl: string + } } page: { actions: {