mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-02 21:12:13 +08:00
* feat(settings): add tunnels viewer UI and management logic * docs: Changelog.md * refactor(notice): remove redundant t() call * refactor(tunnels): make proxy optional and follow current configuration by default * refactor(tunnels): save after close dialog * feat(tunnel): update check * refactor(tunnels): merge import * refactor(tunnels): use ipaddr.js * docs: Changelog.md * refactor(tunnels): enhance validation * feat: add tunnels ti PATCH_CONFIG_INNER * fix: sanitize invalid proxy references in tunnels * refactor: use smartstring alias String * docs: Changelog.md * perf: optimize tunnels proxy validation and collection logic --------- Co-authored-by: Slinetrac <realakayuki@gmail.com> Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
113 lines
2.7 KiB
TypeScript
113 lines
2.7 KiB
TypeScript
import ipaddr from "ipaddr.js";
|
||
|
||
import { debugLog } from "@/utils/debug";
|
||
|
||
export type HostKind = "localhost" | "domain" | "ipv4" | "ipv6";
|
||
|
||
export type ParsedHost = {
|
||
kind: HostKind;
|
||
host: string;
|
||
};
|
||
|
||
export const isValidUrl = (url: string) => {
|
||
try {
|
||
new URL(url);
|
||
return true;
|
||
} catch (e) {
|
||
debugLog(e);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
export const isValidPort = (port: string) => {
|
||
const portNumber = Number(port);
|
||
return Number.isInteger(portNumber) && portNumber > 0 && portNumber < 65536;
|
||
};
|
||
|
||
export const isValidDomain = (domain: string) => {
|
||
try {
|
||
const url = new URL(`http://${domain}`);
|
||
return url.hostname.toLowerCase() === domain.toLowerCase();
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const isLocalhostString = (host: string) => {
|
||
const lowerHost = host.toLowerCase();
|
||
return lowerHost === "localhost";
|
||
};
|
||
|
||
const stripBrackets = (host: string): string =>
|
||
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
||
|
||
export const parseHost = (host: string): ParsedHost | null => {
|
||
// 去除前后空白
|
||
host = host.trim();
|
||
// 优先检测 localhost
|
||
if (isLocalhostString(host)) {
|
||
return { kind: "localhost", host: "localhost" };
|
||
}
|
||
// 除去方括号后检测 IP
|
||
const strippedHost = stripBrackets(host);
|
||
// IPv4
|
||
if (ipaddr.IPv4.isValidFourPartDecimal(strippedHost)) {
|
||
return { kind: "ipv4", host: strippedHost };
|
||
}
|
||
// IPv6
|
||
if (ipaddr.IPv6.isValid(strippedHost)) {
|
||
return { kind: "ipv6", host: strippedHost };
|
||
}
|
||
// 再检测 domain, 防止像 [::1] 这样的合法 IPv6 地址被误判为域名
|
||
if (isValidDomain(host)) {
|
||
return { kind: "domain", host };
|
||
}
|
||
// 都不是
|
||
return null;
|
||
};
|
||
|
||
const LISTEN_CIDRS_V4 = [
|
||
"0.0.0.0/32", // any
|
||
"127.0.0.0/8", // loopback
|
||
"10.0.0.0/8", // RFC1918
|
||
"172.16.0.0/12", // RFC1918
|
||
"192.168.0.0/16", // RFC1918
|
||
];
|
||
|
||
const LISTEN_CIDRS_V6 = [
|
||
"::/128", // any
|
||
"::1/128", // loopback
|
||
"fc00::/7", // ULA(LAN)
|
||
"fe80::/10", // link-local
|
||
];
|
||
|
||
export const parsedLocalhost = (host: string): ParsedHost | null => {
|
||
// 先 parse 一下
|
||
const parsed = parseHost(host);
|
||
if (!parsed) return null;
|
||
// localhost 直接通过
|
||
if (parsed.kind === "localhost") {
|
||
return parsed;
|
||
}
|
||
// IP 则检查是否在允许的 CIDR 列表内
|
||
if (parsed.kind === "ipv4") {
|
||
// IPv4
|
||
const addr = ipaddr.IPv4.parse(parsed.host);
|
||
for (const cidr of LISTEN_CIDRS_V4) {
|
||
if (addr.match(ipaddr.IPv4.parseCIDR(cidr))) {
|
||
return parsed;
|
||
}
|
||
}
|
||
} else if (parsed.kind === "ipv6") {
|
||
// IPv6
|
||
const addr = ipaddr.IPv6.parse(parsed.host);
|
||
for (const cidr of LISTEN_CIDRS_V6) {
|
||
if (addr.match(ipaddr.IPv6.parseCIDR(cidr))) {
|
||
return parsed;
|
||
}
|
||
}
|
||
}
|
||
// domain和都不符合则返回 null
|
||
return null;
|
||
};
|