import { decodeBase64OrOriginal, firstString, getCipher, getIfNotBlank, getIfPresent, isPresent, parseBool, parseRequiredPort, safeDecodeURIComponent, splitOnce, stripUriScheme, trimStr, } from "./helpers"; function parseVmessShadowrocketParams(raw: string): Record { const match = /(^[^?]+?)\/?\?(.*)$/.exec(raw); if (!match) return {}; const [, base64Line, qs] = match; const content = decodeBase64OrOriginal(base64Line); const params: Record = {}; 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 { 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 = {}; 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; }