diff --git a/vite-frontend/src/pages/node/tunnel-monitor-view.tsx b/vite-frontend/src/pages/node/tunnel-monitor-view.tsx index 3eec8a3..b51aeeb 100644 --- a/vite-frontend/src/pages/node/tunnel-monitor-view.tsx +++ b/vite-frontend/src/pages/node/tunnel-monitor-view.tsx @@ -1170,6 +1170,7 @@ export function TunnelMonitorView({ {tunnels.map((tunnel) => { const quality = qualityMap[tunnel.id]; const isEnabled = tunnel.status === 1; + const targetLabel = probeTargetLabel(quality); return (
-
+
出口→测试目标
+
+ {targetLabel} +
{ const quality = qualityMap[tunnel.id]; const isEnabled = tunnel.status === 1; + const targetLabel = probeTargetLabel(quality); return ( - - + +
+ + + {targetLabel} + +
{quality?.timestamp ? ( diff --git a/vite-frontend/src/pages/tunnel/form.ts b/vite-frontend/src/pages/tunnel/form.ts index bb7b4f1..e6b636c 100644 --- a/vite-frontend/src/pages/tunnel/form.ts +++ b/vite-frontend/src/pages/tunnel/form.ts @@ -17,6 +17,93 @@ interface TunnelNodeInput { status: number; } +const isValidProbeIPv4 = (host: string) => { + const parts = host.split("."); + + return ( + parts.length === 4 && + parts.every((part) => { + if (!/^\d+$/.test(part)) { + return false; + } + + const value = Number(part); + + return value >= 0 && value <= 255; + }) + ); +}; + +const isIPv4LikeProbeHost = (host: string) => + /^[0-9.]+$/.test(host) && host.includes("."); + +const isValidProbeIPv6 = (host: string) => { + let value = host; + + if (host.startsWith("[") || host.endsWith("]")) { + if (!host.startsWith("[") || !host.endsWith("]")) { + return false; + } + value = host.slice(1, -1); + } + + if (!value.includes(":") || value.includes("[") || value.includes("]")) { + return false; + } + + try { + const url = new URL(`http://[${value}]`); + + return url.hostname.length > 0; + } catch { + return false; + } +}; + +const isSchemeLikeProbeHost = (host: string) => { + if (isValidProbeIPv6(host)) { + return false; + } + + const colonIndex = host.indexOf(":"); + + if (colonIndex <= 0) { + return false; + } + + return /^[A-Za-z][A-Za-z0-9+.-]*$/.test(host.slice(0, colonIndex)); +}; + +const isValidProbeDomain = (host: string) => { + if (!host || host.length > 253) { + return false; + } + + return host.split(".").every((label) => { + if ( + !label || + label.length > 63 || + label.startsWith("-") || + label.endsWith("-") + ) { + return false; + } + + return /^[A-Za-z0-9-]+$/.test(label); + }); +}; + +const isValidProbeTargetHost = (host: string) => { + if (isValidProbeIPv6(host) || isValidProbeIPv4(host)) { + return true; + } + if (host.includes(":") || isIPv4LikeProbeHost(host)) { + return false; + } + + return isValidProbeDomain(host); +}; + export const createTunnelFormDefaults = () => { return { name: "", @@ -75,8 +162,14 @@ export const validateTunnelForm = ( if (probeHost || hasProbePort) { if (!probeHost) { errors.probeTargetHost = "请输入测试目标 Host"; - } else if (probeHost.includes("://") || /[\s/?#]/.test(probeHost)) { - errors.probeTargetHost = "Host 不能包含协议、空格或路径"; + } else if ( + probeHost.includes("://") || + /[\s/?#]/.test(probeHost) || + isSchemeLikeProbeHost(probeHost) + ) { + errors.probeTargetHost = "Host 不能包含协议、端口、空格或路径"; + } else if (!isValidProbeTargetHost(probeHost)) { + errors.probeTargetHost = "测试目标 Host 格式无效"; } if (!Number.isInteger(probePort) || probePort < 1 || probePort > 65535) {