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;
}