mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
269 lines
7.3 KiB
TypeScript
269 lines
7.3 KiB
TypeScript
import {
|
|
decodeBase64OrOriginal,
|
|
firstString,
|
|
getCipher,
|
|
getIfNotBlank,
|
|
getIfPresent,
|
|
isPresent,
|
|
parseBool,
|
|
parseRequiredPort,
|
|
safeDecodeURIComponent,
|
|
splitOnce,
|
|
stripUriScheme,
|
|
trimStr,
|
|
} from "./helpers";
|
|
|
|
function parseVmessShadowrocketParams(raw: string): Record<string, any> {
|
|
const match = /(^[^?]+?)\/?\?(.*)$/.exec(raw);
|
|
if (!match) return {};
|
|
|
|
const [, base64Line, qs] = match;
|
|
const content = decodeBase64OrOriginal(base64Line);
|
|
const params: Record<string, any> = {};
|
|
|
|
for (const addon of qs.split("&")) {
|
|
if (!addon) continue;
|
|
const [keyRaw, valueRaw] = splitOnce(addon, "=");
|
|
const key = keyRaw.trim();
|
|
if (!key) continue;
|
|
if (valueRaw === undefined) {
|
|
params[key] = true;
|
|
continue;
|
|
}
|
|
const value = safeDecodeURIComponent(valueRaw) ?? valueRaw;
|
|
params[key] = value.includes(",") ? value.split(",") : value;
|
|
}
|
|
|
|
const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
|
|
if (!contentMatch) return params;
|
|
|
|
const [, cipher, uuid, server, port] = contentMatch;
|
|
params.scy = cipher;
|
|
params.id = uuid;
|
|
params.port = port;
|
|
params.add = server;
|
|
return params;
|
|
}
|
|
|
|
function parseVmessParams(decoded: string, raw: string): Record<string, any> {
|
|
try {
|
|
// V2rayN URI format
|
|
return JSON.parse(decoded);
|
|
} catch (e) {
|
|
// Shadowrocket URI format
|
|
console.warn(
|
|
"[URI_VMESS] JSON.parse(content) failed, falling back to Shadowrocket parsing:",
|
|
e,
|
|
);
|
|
return parseVmessShadowrocketParams(raw);
|
|
}
|
|
}
|
|
|
|
function parseVmessQuantumult(content: string): IProxyVmessConfig {
|
|
const partitions = content.split(",").map((p) => p.trim());
|
|
const params: Record<string, string> = {};
|
|
for (const part of partitions) {
|
|
if (part.indexOf("=") !== -1) {
|
|
const [key, val] = splitOnce(part, "=");
|
|
params[key.trim()] = val?.trim() ?? "";
|
|
}
|
|
}
|
|
|
|
const proxy: IProxyVmessConfig = {
|
|
name: partitions[0].split("=")[0].trim(),
|
|
type: "vmess",
|
|
server: partitions[1],
|
|
port: parseRequiredPort(partitions[2], "Invalid vmess uri: invalid port"),
|
|
cipher: getCipher(getIfNotBlank(partitions[3], "auto")),
|
|
uuid: partitions[4].match(/^"(.*)"$/)?.[1] || "",
|
|
tls: params.obfs === "wss",
|
|
udp: parseBool(params["udp-relay"]),
|
|
tfo: parseBool(params["fast-open"]),
|
|
"skip-cert-verify":
|
|
params["tls-verification"] === undefined
|
|
? undefined
|
|
: !parseBool(params["tls-verification"]),
|
|
};
|
|
|
|
if (isPresent(params.obfs)) {
|
|
if (params.obfs === "ws" || params.obfs === "wss") {
|
|
proxy.network = "ws";
|
|
proxy["ws-opts"] = {
|
|
path:
|
|
(getIfNotBlank(params["obfs-path"]) || '"/"').match(
|
|
/^"(.*)"$/,
|
|
)?.[1] || "/",
|
|
headers: {
|
|
Host:
|
|
params["obfs-header"]?.match(/Host:\s*([a-zA-Z0-9-.]*)/)?.[1] || "",
|
|
},
|
|
};
|
|
} else {
|
|
throw new Error(`Unsupported obfs: ${params.obfs}`);
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
export function URI_VMESS(line: string): IProxyVmessConfig {
|
|
const afterScheme = stripUriScheme(line, "vmess", "Invalid vmess uri");
|
|
if (!afterScheme) {
|
|
throw new Error("Invalid vmess uri");
|
|
}
|
|
const raw = afterScheme;
|
|
const content = decodeBase64OrOriginal(raw);
|
|
if (/=\s*vmess/.test(content)) {
|
|
return parseVmessQuantumult(content);
|
|
}
|
|
|
|
const params = parseVmessParams(content, raw);
|
|
const server = params.add;
|
|
const port = parseRequiredPort(
|
|
params.port,
|
|
"Invalid vmess uri: invalid port",
|
|
);
|
|
const tlsValue = params.tls;
|
|
const proxy: IProxyVmessConfig = {
|
|
name:
|
|
trimStr(params.ps) ??
|
|
trimStr(params.remarks) ??
|
|
trimStr(params.remark) ??
|
|
`VMess ${server}:${port}`,
|
|
type: "vmess",
|
|
server,
|
|
port,
|
|
cipher: getCipher(getIfPresent(params.scy, "auto")),
|
|
uuid: params.id,
|
|
tls:
|
|
tlsValue === "tls" ||
|
|
tlsValue === true ||
|
|
tlsValue === 1 ||
|
|
tlsValue === "1" ||
|
|
tlsValue === "true",
|
|
"skip-cert-verify": isPresent(params.verify_cert)
|
|
? !parseBool(params.verify_cert.toString())
|
|
: undefined,
|
|
};
|
|
|
|
proxy.alterId = parseInt(getIfPresent(params.aid ?? params.alterId, 0), 10);
|
|
|
|
if (proxy.tls && params.sni) {
|
|
proxy.servername = params.sni;
|
|
}
|
|
|
|
let httpupgrade = false;
|
|
if (params.net === "ws" || params.obfs === "websocket") {
|
|
proxy.network = "ws";
|
|
} else if (
|
|
["http"].includes(params.net) ||
|
|
["http"].includes(params.obfs) ||
|
|
["http"].includes(params.type)
|
|
) {
|
|
proxy.network = "http";
|
|
} else if (["grpc"].includes(params.net)) {
|
|
proxy.network = "grpc";
|
|
} else if (params.net === "httpupgrade") {
|
|
proxy.network = "ws";
|
|
httpupgrade = true;
|
|
} else if (params.net === "h2" || proxy.network === "h2") {
|
|
proxy.network = "h2";
|
|
}
|
|
|
|
if (proxy.network) {
|
|
let transportHost: any = params.host ?? params.obfsParam;
|
|
if (typeof transportHost === "string") {
|
|
try {
|
|
const parsedObfs = JSON.parse(transportHost);
|
|
const parsedHost = parsedObfs?.Host;
|
|
if (parsedHost) {
|
|
transportHost = parsedHost;
|
|
}
|
|
} catch (e) {
|
|
console.warn("[URI_VMESS] transportHost JSON.parse failed:", e);
|
|
}
|
|
}
|
|
|
|
const transportPath: any = params.path;
|
|
const hostFirst = getIfNotBlank(firstString(transportHost));
|
|
const pathFirst = getIfNotBlank(firstString(transportPath));
|
|
|
|
switch (proxy.network) {
|
|
case "grpc": {
|
|
if (!hostFirst && !pathFirst) {
|
|
delete proxy.network;
|
|
break;
|
|
}
|
|
const serviceName = getIfNotBlank(pathFirst);
|
|
if (serviceName) {
|
|
proxy["grpc-opts"] = { "grpc-service-name": serviceName };
|
|
}
|
|
break;
|
|
}
|
|
case "h2": {
|
|
if (!hostFirst && !pathFirst) {
|
|
delete proxy.network;
|
|
break;
|
|
}
|
|
const h2Opts: H2Options = {};
|
|
if (hostFirst) h2Opts.host = hostFirst;
|
|
if (pathFirst) h2Opts.path = pathFirst;
|
|
if (Object.keys(h2Opts).length > 0) {
|
|
proxy["h2-opts"] = h2Opts;
|
|
}
|
|
break;
|
|
}
|
|
case "http": {
|
|
const hosts = Array.isArray(transportHost)
|
|
? transportHost
|
|
.map((h: any) => String(h).trim())
|
|
.filter((h: string) => h)
|
|
: hostFirst
|
|
? [hostFirst]
|
|
: undefined;
|
|
|
|
let paths = Array.isArray(transportPath)
|
|
? transportPath
|
|
.map((p: any) => String(p).trim())
|
|
.filter((p: string) => p)
|
|
: pathFirst
|
|
? [pathFirst]
|
|
: [];
|
|
|
|
if (paths.length === 0) paths = ["/"];
|
|
|
|
const httpOpts: HttpOptions = { path: paths };
|
|
if (hosts && hosts.length > 0) {
|
|
httpOpts.headers = { Host: hosts };
|
|
}
|
|
proxy["http-opts"] = httpOpts;
|
|
break;
|
|
}
|
|
case "ws": {
|
|
if (!hostFirst && !pathFirst && !httpupgrade) {
|
|
delete proxy.network;
|
|
break;
|
|
}
|
|
const wsOpts: WsOptions = {
|
|
path: pathFirst,
|
|
headers: hostFirst ? { Host: hostFirst } : undefined,
|
|
};
|
|
if (httpupgrade) {
|
|
wsOpts["v2ray-http-upgrade"] = true;
|
|
wsOpts["v2ray-http-upgrade-fast-open"] = true;
|
|
}
|
|
proxy["ws-opts"] = wsOpts;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (proxy.tls && !proxy.servername && hostFirst) {
|
|
proxy.servername = hostFirst;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|