refactor: add keys to icons in routers for improved rendering and performance refactor: optimize RegExp polyfill by using Object.prototype.hasOwnProperty.call refactor: reorder imports in chain-proxy-provider for consistency refactor: remove unused "obfs-opts" property from IProxySnellConfig interface refactor: reorganize imports and enhance refresh logic in app data provider refactor: re-enable prop-types linting for better type safety in BaseDialog component refactor: update dependencies in effect hooks for improved stability and performance
1182 lines
31 KiB
TypeScript
1182 lines
31 KiB
TypeScript
export default function parseUri(uri: string): IProxyConfig {
|
|
const head = uri.split("://")[0];
|
|
switch (head) {
|
|
case "ss":
|
|
return URI_SS(uri);
|
|
case "ssr":
|
|
return URI_SSR(uri);
|
|
case "vmess":
|
|
return URI_VMESS(uri);
|
|
case "vless":
|
|
return URI_VLESS(uri);
|
|
case "trojan":
|
|
return URI_Trojan(uri);
|
|
case "hysteria2":
|
|
return URI_Hysteria2(uri);
|
|
case "hy2":
|
|
return URI_Hysteria2(uri);
|
|
case "hysteria":
|
|
return URI_Hysteria(uri);
|
|
case "hy":
|
|
return URI_Hysteria(uri);
|
|
case "tuic":
|
|
return URI_TUIC(uri);
|
|
case "wireguard":
|
|
return URI_Wireguard(uri);
|
|
case "wg":
|
|
return URI_Wireguard(uri);
|
|
case "http":
|
|
return URI_HTTP(uri);
|
|
case "socks5":
|
|
return URI_SOCKS(uri);
|
|
default:
|
|
throw Error(`Unknown uri type: ${head}`);
|
|
}
|
|
}
|
|
|
|
function getIfNotBlank(
|
|
value: string | undefined,
|
|
dft?: string,
|
|
): string | undefined {
|
|
return value && value.trim() !== "" ? value : dft;
|
|
}
|
|
|
|
function getIfPresent(value: any, dft?: any): any {
|
|
return value ? value : dft;
|
|
}
|
|
|
|
function isPresent(value: any): boolean {
|
|
return value !== null && value !== undefined;
|
|
}
|
|
|
|
function trimStr(str: string | undefined): string | undefined {
|
|
return str ? str.trim() : str;
|
|
}
|
|
|
|
function isIPv4(address: string): boolean {
|
|
// Check if the address is IPv4
|
|
const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
|
return ipv4Regex.test(address);
|
|
}
|
|
|
|
function isIPv6(address: string): boolean {
|
|
// Check if the address is IPv6 - simplified regex to avoid backreference issues
|
|
const ipv6Regex =
|
|
/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^::1$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/;
|
|
return ipv6Regex.test(address);
|
|
}
|
|
|
|
function decodeBase64OrOriginal(str: string): string {
|
|
try {
|
|
return atob(str);
|
|
} catch {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
function getCipher(str: string | undefined) {
|
|
switch (str) {
|
|
case "none":
|
|
return "none";
|
|
case "auto":
|
|
return "auto";
|
|
case "dummy":
|
|
return "dummy";
|
|
case "aes-128-gcm":
|
|
return "aes-128-gcm";
|
|
case "aes-192-gcm":
|
|
return "aes-192-gcm";
|
|
case "aes-256-gcm":
|
|
return "aes-256-gcm";
|
|
case "lea-128-gcm":
|
|
return "lea-128-gcm";
|
|
case "lea-192-gcm":
|
|
return "lea-192-gcm";
|
|
case "lea-256-gcm":
|
|
return "lea-256-gcm";
|
|
case "aes-128-gcm-siv":
|
|
return "aes-128-gcm-siv";
|
|
case "aes-256-gcm-siv":
|
|
return "aes-256-gcm-siv";
|
|
case "2022-blake3-aes-128-gcm":
|
|
return "2022-blake3-aes-128-gcm";
|
|
case "2022-blake3-aes-256-gcm":
|
|
return "2022-blake3-aes-256-gcm";
|
|
case "aes-128-cfb":
|
|
return "aes-128-cfb";
|
|
case "aes-192-cfb":
|
|
return "aes-192-cfb";
|
|
case "aes-256-cfb":
|
|
return "aes-256-cfb";
|
|
case "aes-128-ctr":
|
|
return "aes-128-ctr";
|
|
case "aes-192-ctr":
|
|
return "aes-192-ctr";
|
|
case "aes-256-ctr":
|
|
return "aes-256-ctr";
|
|
case "chacha20":
|
|
return "chacha20";
|
|
case "chacha20-poly1305":
|
|
return "chacha20-ietf-poly1305";
|
|
case "chacha20-ietf":
|
|
return "chacha20-ietf";
|
|
case "chacha20-ietf-poly1305":
|
|
return "chacha20-ietf-poly1305";
|
|
case "2022-blake3-chacha20-poly1305":
|
|
return "2022-blake3-chacha20-poly1305";
|
|
case "rabbit128-poly1305":
|
|
return "rabbit128-poly1305";
|
|
case "xchacha20-ietf-poly1305":
|
|
return "xchacha20-ietf-poly1305";
|
|
case "xchacha20":
|
|
return "xchacha20";
|
|
case "aegis-128l":
|
|
return "aegis-128l";
|
|
case "aegis-256":
|
|
return "aegis-256";
|
|
case "aez-384":
|
|
return "aez-384";
|
|
case "deoxys-ii-256-128":
|
|
return "deoxys-ii-256-128";
|
|
case "rc4-md5":
|
|
return "rc4-md5";
|
|
case undefined:
|
|
return "none";
|
|
default:
|
|
return "auto";
|
|
}
|
|
}
|
|
|
|
function URI_SS(line: string): IProxyShadowsocksConfig {
|
|
// parse url
|
|
let content = line.split("ss://")[1];
|
|
|
|
const proxy: IProxyShadowsocksConfig = {
|
|
name: decodeURIComponent(line.split("#")[1]).trim(),
|
|
type: "ss",
|
|
server: "",
|
|
port: 0,
|
|
};
|
|
content = content.split("#")[0]; // strip proxy name
|
|
// handle IPV4 and IPV6
|
|
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
|
let userInfoStr = decodeBase64OrOriginal(content.split("@")[0]);
|
|
let query = "";
|
|
if (!serverAndPortArray) {
|
|
if (content.includes("?")) {
|
|
const parsed = content.match(/^(.*)(\?.*)$/);
|
|
content = parsed?.[1] ?? "";
|
|
query = parsed?.[2] ?? "";
|
|
}
|
|
content = decodeBase64OrOriginal(content);
|
|
if (query) {
|
|
if (/(&|\?)v2ray-plugin=/.test(query)) {
|
|
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
|
|
const v2rayPlugin = parsed![2];
|
|
if (v2rayPlugin) {
|
|
proxy.plugin = "v2ray-plugin";
|
|
proxy["plugin-opts"] = JSON.parse(
|
|
decodeBase64OrOriginal(v2rayPlugin),
|
|
);
|
|
}
|
|
}
|
|
content = `${content}${query}`;
|
|
}
|
|
userInfoStr = content.split("@")[0];
|
|
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
|
}
|
|
const serverAndPort = serverAndPortArray?.[1];
|
|
const portIdx = serverAndPort?.lastIndexOf(":") ?? 0;
|
|
proxy.server = serverAndPort?.substring(0, portIdx) ?? "";
|
|
proxy.port = parseInt(
|
|
`${serverAndPort?.substring(portIdx + 1)}`.match(/\d+/)?.[0] ?? "",
|
|
);
|
|
const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
|
|
proxy.cipher = getCipher(userInfo?.[1]);
|
|
proxy.password = userInfo?.[2];
|
|
|
|
// handle obfs
|
|
const idx = content.indexOf("?plugin=");
|
|
if (idx !== -1) {
|
|
const pluginInfo = (
|
|
"plugin=" + decodeURIComponent(content.split("?plugin=")[1].split("&")[0])
|
|
).split(";");
|
|
const params: Record<string, any> = {};
|
|
for (const item of pluginInfo) {
|
|
const [key, val] = item.split("=");
|
|
if (key) params[key] = val || true; // some options like "tls" will not have value
|
|
}
|
|
switch (params.plugin) {
|
|
case "obfs-local":
|
|
case "simple-obfs":
|
|
proxy.plugin = "obfs";
|
|
proxy["plugin-opts"] = {
|
|
mode: params.obfs,
|
|
host: getIfNotBlank(params["obfs-host"]),
|
|
};
|
|
break;
|
|
case "v2ray-plugin":
|
|
proxy.plugin = "v2ray-plugin";
|
|
proxy["plugin-opts"] = {
|
|
mode: "websocket",
|
|
host: getIfNotBlank(params["obfs-host"]),
|
|
path: getIfNotBlank(params.path),
|
|
tls: getIfPresent(params.tls),
|
|
};
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported plugin option: ${params.plugin}`);
|
|
}
|
|
}
|
|
if (/(&|\?)uot=(1|true)/i.test(query)) {
|
|
proxy["udp-over-tcp"] = true;
|
|
}
|
|
if (/(&|\?)tfo=(1|true)/i.test(query)) {
|
|
proxy.tfo = true;
|
|
}
|
|
return proxy;
|
|
}
|
|
|
|
function URI_SSR(line: string): IProxyshadowsocksRConfig {
|
|
line = decodeBase64OrOriginal(line.split("ssr://")[1]);
|
|
|
|
// handle IPV6 & IPV4 format
|
|
let splitIdx = line.indexOf(":origin");
|
|
if (splitIdx === -1) {
|
|
splitIdx = line.indexOf(":auth_");
|
|
}
|
|
const serverAndPort = line.substring(0, splitIdx);
|
|
const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
|
|
const port = parseInt(
|
|
serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1),
|
|
);
|
|
|
|
const params = line
|
|
.substring(splitIdx + 1)
|
|
.split("/?")[0]
|
|
.split(":");
|
|
let proxy: IProxyshadowsocksRConfig = {
|
|
name: "SSR",
|
|
type: "ssr",
|
|
server,
|
|
port,
|
|
protocol: params[0],
|
|
cipher: getCipher(params[1]),
|
|
obfs: params[2],
|
|
password: decodeBase64OrOriginal(params[3]),
|
|
};
|
|
|
|
// get other params
|
|
const other_params: Record<string, string> = {};
|
|
const paramsArray = line.split("/?")[1]?.split("&") || [];
|
|
for (const item of paramsArray) {
|
|
const [key, val] = item.split("=");
|
|
if (val?.trim().length > 0) {
|
|
other_params[key] = val.trim();
|
|
}
|
|
}
|
|
|
|
proxy = {
|
|
...proxy,
|
|
name: other_params.remarks
|
|
? decodeBase64OrOriginal(other_params.remarks).trim()
|
|
: (proxy.server ?? ""),
|
|
"protocol-param": getIfNotBlank(
|
|
decodeBase64OrOriginal(other_params.protoparam || "").replace(/\s/g, ""),
|
|
),
|
|
"obfs-param": getIfNotBlank(
|
|
decodeBase64OrOriginal(other_params.obfsparam || "").replace(/\s/g, ""),
|
|
),
|
|
};
|
|
return proxy;
|
|
}
|
|
|
|
function URI_VMESS(line: string): IProxyVmessConfig {
|
|
line = line.split("vmess://")[1];
|
|
let content = decodeBase64OrOriginal(line);
|
|
if (/=\s*vmess/.test(content)) {
|
|
// Quantumult VMess URI format
|
|
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] = part.split("=");
|
|
params[key.trim()] = val.trim();
|
|
}
|
|
}
|
|
|
|
const proxy: IProxyVmessConfig = {
|
|
name: partitions[0].split("=")[0].trim(),
|
|
type: "vmess",
|
|
server: partitions[1],
|
|
port: parseInt(partitions[2], 10),
|
|
cipher: getCipher(getIfNotBlank(partitions[3], "auto")),
|
|
uuid: partitions[4].match(/^"(.*)"$/)?.[1] || "",
|
|
tls: params.obfs === "wss",
|
|
udp: getIfPresent(params["udp-relay"]),
|
|
tfo: getIfPresent(params["fast-open"]),
|
|
"skip-cert-verify": isPresent(params["tls-verification"])
|
|
? !params["tls-verification"]
|
|
: undefined,
|
|
};
|
|
|
|
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;
|
|
} else {
|
|
let params: Record<string, any> = {};
|
|
|
|
try {
|
|
// V2rayN URI format
|
|
params = JSON.parse(content);
|
|
} catch (e) {
|
|
// Shadowrocket URI format
|
|
console.warn(
|
|
"[URI_VMESS] JSON.parse(content) failed, falling back to Shadowrocket parsing:",
|
|
e,
|
|
);
|
|
const match = /(^[^?]+?)\/?\?(.*)$/.exec(line);
|
|
if (match) {
|
|
const [_, base64Line, qs] = match;
|
|
content = decodeBase64OrOriginal(base64Line);
|
|
|
|
for (const addon of qs.split("&")) {
|
|
const [key, valueRaw] = addon.split("=");
|
|
const value = decodeURIComponent(valueRaw);
|
|
if (value.indexOf(",") === -1) {
|
|
params[key] = value;
|
|
} else {
|
|
params[key] = value.split(",");
|
|
}
|
|
}
|
|
|
|
const contentMatch = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
|
|
|
|
if (contentMatch) {
|
|
const [__, cipher, uuid, server, port] = contentMatch;
|
|
|
|
params.scy = cipher;
|
|
params.id = uuid;
|
|
params.port = port;
|
|
params.add = server;
|
|
}
|
|
}
|
|
}
|
|
|
|
const server = params.add;
|
|
const port = parseInt(getIfPresent(params.port), 10);
|
|
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: ["tls", true, 1, "1"].includes(params.tls),
|
|
"skip-cert-verify": isPresent(params.verify_cert)
|
|
? !params.verify_cert
|
|
: 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 = params.host ?? params.obfsParam;
|
|
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);
|
|
// ignore JSON parse errors
|
|
}
|
|
|
|
let transportPath = params.path;
|
|
if (proxy.network === "http") {
|
|
if (transportHost) {
|
|
transportHost = Array.isArray(transportHost)
|
|
? transportHost[0]
|
|
: transportHost;
|
|
}
|
|
if (transportPath) {
|
|
transportPath = Array.isArray(transportPath)
|
|
? transportPath[0]
|
|
: transportPath;
|
|
} else {
|
|
transportPath = "/";
|
|
}
|
|
}
|
|
|
|
if (transportPath || transportHost) {
|
|
if (["grpc"].includes(proxy.network)) {
|
|
proxy[`grpc-opts`] = {
|
|
"grpc-service-name": getIfNotBlank(transportPath),
|
|
};
|
|
} else {
|
|
const opts: Record<string, any> = {
|
|
path: getIfNotBlank(transportPath),
|
|
headers: { Host: getIfNotBlank(transportHost) },
|
|
};
|
|
if (httpupgrade) {
|
|
opts["v2ray-http-upgrade"] = true;
|
|
opts["v2ray-http-upgrade-fast-open"] = true;
|
|
}
|
|
switch (proxy.network) {
|
|
case "ws":
|
|
proxy["ws-opts"] = opts;
|
|
break;
|
|
case "http":
|
|
proxy["http-opts"] = opts;
|
|
break;
|
|
case "h2":
|
|
proxy["h2-opts"] = opts;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
delete proxy.network;
|
|
}
|
|
|
|
if (proxy.tls && !proxy.servername && transportHost) {
|
|
proxy.servername = transportHost;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* VLess URL Decode.
|
|
*/
|
|
function URI_VLESS(line: string): IProxyVlessConfig {
|
|
line = line.split("vless://")[1];
|
|
let isShadowrocket;
|
|
let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
if (!parsed) {
|
|
const [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line)!;
|
|
line = `${atob(base64)}${other}`;
|
|
parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
isShadowrocket = true;
|
|
}
|
|
const [, uuidRaw, server, portStr, , addons = "", nameRaw] = parsed;
|
|
let uuid = uuidRaw;
|
|
let name = nameRaw;
|
|
if (isShadowrocket) {
|
|
uuid = uuidRaw.replace(/^.*?:/g, "");
|
|
}
|
|
|
|
const port = parseInt(portStr, 10);
|
|
uuid = decodeURIComponent(uuid);
|
|
name = decodeURIComponent(name);
|
|
|
|
const proxy: IProxyVlessConfig = {
|
|
type: "vless",
|
|
name: "",
|
|
server,
|
|
port,
|
|
uuid,
|
|
};
|
|
|
|
const params: Record<string, string> = {};
|
|
for (const addon of addons.split("&")) {
|
|
const [key, valueRaw] = addon.split("=");
|
|
const value = decodeURIComponent(valueRaw);
|
|
params[key] = value;
|
|
}
|
|
|
|
proxy.name =
|
|
trimStr(name) ??
|
|
trimStr(params.remarks) ??
|
|
trimStr(params.remark) ??
|
|
`VLESS ${server}:${port}`;
|
|
|
|
proxy.tls = (params.security && params.security !== "none") || undefined;
|
|
if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
|
|
proxy.tls = true;
|
|
params.security = params.security ?? "reality";
|
|
}
|
|
proxy.servername = params.sni || params.peer;
|
|
proxy.flow = params.flow ? "xtls-rprx-vision" : undefined;
|
|
|
|
proxy["client-fingerprint"] = params.fp as ClientFingerprint;
|
|
proxy.alpn = params.alpn ? params.alpn.split(",") : undefined;
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.allowInsecure);
|
|
|
|
if (["reality"].includes(params.security)) {
|
|
const opts: IProxyVlessConfig["reality-opts"] = {};
|
|
if (params.pbk) {
|
|
opts["public-key"] = params.pbk;
|
|
}
|
|
if (params.sid) {
|
|
opts["short-id"] = params.sid;
|
|
}
|
|
if (Object.keys(opts).length > 0) {
|
|
proxy["reality-opts"] = opts;
|
|
}
|
|
}
|
|
|
|
let httpupgrade = false;
|
|
proxy["ws-opts"] = {
|
|
headers: undefined,
|
|
path: undefined,
|
|
};
|
|
|
|
proxy["http-opts"] = {
|
|
headers: undefined,
|
|
path: undefined,
|
|
};
|
|
proxy["grpc-opts"] = {};
|
|
|
|
if (params.headerType === "http") {
|
|
proxy.network = "http";
|
|
} else if (params.type === "ws") {
|
|
proxy.network = "ws";
|
|
httpupgrade = true;
|
|
} else {
|
|
proxy.network = ["tcp", "ws", "http", "grpc", "h2"].includes(params.type)
|
|
? (params.type as NetworkType)
|
|
: "tcp";
|
|
}
|
|
if (!proxy.network && isShadowrocket && params.obfs) {
|
|
switch (params.type) {
|
|
case "sw":
|
|
proxy.network = "ws";
|
|
break;
|
|
case "http":
|
|
proxy.network = "http";
|
|
break;
|
|
case "h2":
|
|
proxy.network = "h2";
|
|
break;
|
|
case "grpc":
|
|
proxy.network = "grpc";
|
|
break;
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (["websocket"].includes(proxy.network)) {
|
|
proxy.network = "ws";
|
|
}
|
|
if (proxy.network && !["tcp", "none"].includes(proxy.network)) {
|
|
const opts: Record<string, any> = {};
|
|
const host = params.host ?? params.obfsParam;
|
|
if (host) {
|
|
if (params.obfsParam) {
|
|
try {
|
|
const parsed = JSON.parse(host);
|
|
opts.headers = parsed;
|
|
} catch (e) {
|
|
console.warn("[URI_VLESS] host JSON.parse failed:", e);
|
|
opts.headers = { Host: host };
|
|
}
|
|
} else {
|
|
opts.headers = { Host: host };
|
|
}
|
|
}
|
|
if (params.path) {
|
|
opts.path = params.path;
|
|
}
|
|
if (httpupgrade) {
|
|
opts["v2ray-http-upgrade"] = true;
|
|
opts["v2ray-http-upgrade-fast-open"] = true;
|
|
}
|
|
if (Object.keys(opts).length > 0) {
|
|
proxy[`ws-opts`] = opts;
|
|
}
|
|
}
|
|
|
|
if (proxy.tls && !proxy.servername) {
|
|
if (proxy.network === "ws") {
|
|
proxy.servername = proxy["ws-opts"]?.headers?.Host;
|
|
} else if (proxy.network === "http") {
|
|
const httpHost = proxy["http-opts"]?.headers?.Host;
|
|
proxy.servername = Array.isArray(httpHost) ? httpHost[0] : httpHost;
|
|
}
|
|
}
|
|
return proxy;
|
|
}
|
|
|
|
function URI_Trojan(line: string): IProxyTrojanConfig {
|
|
line = line.split("trojan://")[1];
|
|
const [, passwordRaw, server, , port, , addons = "", nameRaw] =
|
|
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
|
|
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
|
|
let password = passwordRaw;
|
|
password = decodeURIComponent(password);
|
|
|
|
let name = nameRaw;
|
|
const decodedName = trimStr(decodeURIComponent(name));
|
|
|
|
name = decodedName ?? `Trojan ${server}:${portNum}`;
|
|
const proxy: IProxyTrojanConfig = {
|
|
type: "trojan",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
password,
|
|
};
|
|
let host = "";
|
|
let path = "";
|
|
|
|
for (const addon of addons.split("&")) {
|
|
const [key, valueRaw] = addon.split("=");
|
|
const value = decodeURIComponent(valueRaw);
|
|
switch (key) {
|
|
case "type":
|
|
if (["ws", "h2"].includes(value)) {
|
|
proxy.network = value as NetworkType;
|
|
} else {
|
|
proxy.network = "tcp";
|
|
}
|
|
break;
|
|
case "host":
|
|
host = value;
|
|
break;
|
|
case "path":
|
|
path = value;
|
|
break;
|
|
case "alpn":
|
|
proxy["alpn"] = value ? value.split(",") : undefined;
|
|
break;
|
|
case "sni":
|
|
proxy["sni"] = value;
|
|
break;
|
|
case "skip-cert-verify":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "fingerprint":
|
|
proxy["fingerprint"] = value;
|
|
break;
|
|
case "fp":
|
|
proxy["fingerprint"] = value;
|
|
break;
|
|
case "encryption":
|
|
{
|
|
const encryption = value.split(";");
|
|
if (encryption.length === 3) {
|
|
proxy["ss-opts"] = {
|
|
enabled: true,
|
|
method: encryption[1],
|
|
password: encryption[2],
|
|
};
|
|
}
|
|
}
|
|
break;
|
|
case "client-fingerprint":
|
|
proxy["client-fingerprint"] = value as ClientFingerprint;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (proxy.network === "ws") {
|
|
proxy["ws-opts"] = {
|
|
headers: { Host: host },
|
|
path,
|
|
} as WsOptions;
|
|
} else if (proxy.network === "grpc") {
|
|
proxy["grpc-opts"] = {
|
|
"grpc-service-name": path,
|
|
} as GrpcOptions;
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_Hysteria2(line: string): IProxyHysteria2Config {
|
|
line = line.split(/(hysteria2|hy2):\/\//)[2];
|
|
|
|
const [, passwordRaw, server, , port, , addons = "", nameRaw] =
|
|
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
const password = decodeURIComponent(passwordRaw);
|
|
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
|
|
const name = decodedName ?? `Hysteria2 ${server}:${port}`;
|
|
|
|
const proxy: IProxyHysteria2Config = {
|
|
type: "hysteria2",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
password,
|
|
};
|
|
|
|
const params: Record<string, string> = {};
|
|
for (const addon of addons.split("&")) {
|
|
const [key, valueRaw] = addon.split("=");
|
|
let value = valueRaw;
|
|
value = decodeURIComponent(valueRaw);
|
|
params[key] = value;
|
|
}
|
|
|
|
proxy.sni = params.sni;
|
|
if (!proxy.sni && params.peer) {
|
|
proxy.sni = params.peer;
|
|
}
|
|
if (params.obfs && params.obfs !== "none") {
|
|
proxy.obfs = params.obfs;
|
|
}
|
|
|
|
proxy.ports = params.mport;
|
|
proxy["obfs-password"] = params["obfs-password"];
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(params.insecure);
|
|
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
|
|
proxy.fingerprint = params.pinSHA256;
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_Hysteria(line: string): IProxyHysteriaConfig {
|
|
line = line.split(/(hysteria|hy):\/\//)[2];
|
|
const [, server, , port, , addons = "", nameRaw] =
|
|
/^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
|
|
const name = decodedName ?? `Hysteria ${server}:${port}`;
|
|
|
|
const proxy: IProxyHysteriaConfig = {
|
|
type: "hysteria",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
};
|
|
const params: Record<string, string> = {};
|
|
|
|
for (const addon of addons.split("&")) {
|
|
let [key, value] = addon.split("=");
|
|
key = key.replace(/_/, "-");
|
|
value = decodeURIComponent(value);
|
|
switch (key) {
|
|
case "alpn":
|
|
proxy["alpn"] = value ? value.split(",") : undefined;
|
|
break;
|
|
case "insecure":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "auth":
|
|
proxy["auth-str"] = value;
|
|
break;
|
|
case "mport":
|
|
proxy["ports"] = value;
|
|
break;
|
|
case "obfsParam":
|
|
proxy["obfs"] = value;
|
|
break;
|
|
case "upmbps":
|
|
proxy["up"] = value;
|
|
break;
|
|
case "downmbps":
|
|
proxy["down"] = value;
|
|
break;
|
|
case "obfs":
|
|
proxy["obfs"] = value || "";
|
|
break;
|
|
case "fast-open":
|
|
proxy["fast-open"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "peer":
|
|
proxy["fast-open"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "recv-window-conn":
|
|
proxy["recv-window-conn"] = parseInt(value);
|
|
break;
|
|
case "recv-window":
|
|
proxy["recv-window"] = parseInt(value);
|
|
break;
|
|
case "ca":
|
|
proxy["ca"] = value;
|
|
break;
|
|
case "ca-str":
|
|
proxy["ca-str"] = value;
|
|
break;
|
|
case "disable-mtu-discovery":
|
|
proxy["disable-mtu-discovery"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "fingerprint":
|
|
proxy["fingerprint"] = value;
|
|
break;
|
|
case "protocol":
|
|
proxy["protocol"] = value;
|
|
break;
|
|
case "sni":
|
|
proxy["sni"] = value;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!proxy.sni && params.peer) {
|
|
proxy.sni = params.peer;
|
|
}
|
|
if (!proxy["fast-open"] && params["fast-open"]) {
|
|
proxy["fast-open"] = true;
|
|
}
|
|
if (!proxy.protocol) {
|
|
proxy.protocol = "udp";
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_TUIC(line: string): IProxyTuicConfig {
|
|
line = line.split(/tuic:\/\//)[1];
|
|
|
|
const [, uuid, passwordRaw, server, , port, , addons = "", nameRaw] =
|
|
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || [];
|
|
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
const password = decodeURIComponent(passwordRaw);
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
|
|
const name = decodedName ?? `TUIC ${server}:${port}`;
|
|
|
|
const proxy: IProxyTuicConfig = {
|
|
type: "tuic",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
password,
|
|
uuid,
|
|
};
|
|
|
|
for (const addon of addons.split("&")) {
|
|
let [key, value] = addon.split("=");
|
|
key = key.replace(/_/, "-");
|
|
value = decodeURIComponent(value);
|
|
switch (key) {
|
|
case "token":
|
|
proxy["token"] = value;
|
|
break;
|
|
case "ip":
|
|
proxy["ip"] = value;
|
|
break;
|
|
case "heartbeat-interval":
|
|
proxy["heartbeat-interval"] = parseInt(value);
|
|
break;
|
|
case "alpn":
|
|
proxy["alpn"] = value ? value.split(",") : undefined;
|
|
break;
|
|
case "disable-sni":
|
|
proxy["disable-sni"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "reduce-rtt":
|
|
proxy["reduce-rtt"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "request-timeout":
|
|
proxy["request-timeout"] = parseInt(value);
|
|
break;
|
|
case "udp-relay-mode":
|
|
proxy["udp-relay-mode"] = value;
|
|
break;
|
|
case "congestion-controller":
|
|
proxy["congestion-controller"] = value;
|
|
break;
|
|
case "max-udp-relay-packet-size":
|
|
proxy["max-udp-relay-packet-size"] = parseInt(value);
|
|
break;
|
|
case "fast-open":
|
|
proxy["fast-open"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "skip-cert-verify":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "max-open-streams":
|
|
proxy["max-open-streams"] = parseInt(value);
|
|
break;
|
|
case "sni":
|
|
proxy["sni"] = value;
|
|
break;
|
|
case "allow-insecure":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_Wireguard(line: string): IProxyWireguardConfig {
|
|
line = line.split(/(wireguard|wg):\/\//)[2];
|
|
const [, , privateKeyRaw, server, , port, , addons = "", nameRaw] =
|
|
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
const privateKey = decodeURIComponent(privateKeyRaw);
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
|
|
const name = decodedName ?? `WireGuard ${server}:${port}`;
|
|
const proxy: IProxyWireguardConfig = {
|
|
type: "wireguard",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
"private-key": privateKey,
|
|
udp: true,
|
|
};
|
|
for (const addon of addons.split("&")) {
|
|
let [key, value] = addon.split("=");
|
|
key = key.replace(/_/, "-");
|
|
value = decodeURIComponent(value);
|
|
switch (key) {
|
|
case "address":
|
|
case "ip":
|
|
value.split(",").map((i) => {
|
|
const ip = i
|
|
.trim()
|
|
.replace(/\/\d+$/, "")
|
|
.replace(/^\[/, "")
|
|
.replace(/\]$/, "");
|
|
if (isIPv4(ip)) {
|
|
proxy.ip = ip;
|
|
} else if (isIPv6(ip)) {
|
|
proxy.ipv6 = ip;
|
|
}
|
|
});
|
|
break;
|
|
case "publickey":
|
|
proxy["public-key"] = value;
|
|
break;
|
|
case "allowed-ips":
|
|
proxy["allowed-ips"] = value.split(",");
|
|
break;
|
|
case "pre-shared-key":
|
|
proxy["pre-shared-key"] = value;
|
|
break;
|
|
case "reserved":
|
|
{
|
|
const parsed = value
|
|
.split(",")
|
|
.map((i) => parseInt(i.trim(), 10))
|
|
.filter((i) => Number.isInteger(i));
|
|
if (parsed.length === 3) {
|
|
proxy["reserved"] = parsed;
|
|
}
|
|
}
|
|
break;
|
|
case "udp":
|
|
proxy["udp"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "mtu":
|
|
proxy.mtu = parseInt(value.trim(), 10);
|
|
break;
|
|
case "dialer-proxy":
|
|
proxy["dialer-proxy"] = value;
|
|
break;
|
|
case "remote-dns-resolve":
|
|
proxy["remote-dns-resolve"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "dns":
|
|
proxy.dns = value.split(",");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_HTTP(line: string): IProxyHttpConfig {
|
|
line = line.split(/(http|https):\/\//)[2];
|
|
const [, , authRaw, server, , port, , addons = "", nameRaw] =
|
|
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
let auth = authRaw;
|
|
|
|
if (auth) {
|
|
auth = decodeURIComponent(auth);
|
|
}
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
|
|
const name = decodedName ?? `HTTP ${server}:${portNum}`;
|
|
const proxy: IProxyHttpConfig = {
|
|
type: "http",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
};
|
|
if (auth) {
|
|
const [username, password] = auth.split(":");
|
|
proxy.username = username;
|
|
proxy.password = password;
|
|
}
|
|
|
|
for (const addon of addons.split("&")) {
|
|
let [key, value] = addon.split("=");
|
|
key = key.replace(/_/, "-");
|
|
value = decodeURIComponent(value);
|
|
switch (key) {
|
|
case "tls":
|
|
proxy.tls = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "fingerprint":
|
|
proxy["fingerprint"] = value;
|
|
break;
|
|
case "skip-cert-verify":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "ip-version":
|
|
if (
|
|
["dual", "ipv4", "ipv6", "ipv4-prefer", "ipv6-prefer"].includes(value)
|
|
) {
|
|
proxy["ip-version"] = value as
|
|
| "dual"
|
|
| "ipv4"
|
|
| "ipv6"
|
|
| "ipv4-prefer"
|
|
| "ipv6-prefer";
|
|
} else {
|
|
proxy["ip-version"] = "dual";
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
function URI_SOCKS(line: string): IProxySocks5Config {
|
|
line = line.split(/socks5:\/\//)[1];
|
|
const [, , authRaw, server, , port, , addons = "", nameRaw] =
|
|
/^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line)!;
|
|
|
|
let portNum = parseInt(`${port}`, 10);
|
|
if (isNaN(portNum)) {
|
|
portNum = 443;
|
|
}
|
|
|
|
let auth = authRaw;
|
|
if (auth) {
|
|
auth = decodeURIComponent(auth);
|
|
}
|
|
const decodedName = trimStr(decodeURIComponent(nameRaw));
|
|
const name = decodedName ?? `SOCKS5 ${server}:${portNum}`;
|
|
const proxy: IProxySocks5Config = {
|
|
type: "socks5",
|
|
name,
|
|
server,
|
|
port: portNum,
|
|
};
|
|
if (auth) {
|
|
const [username, password] = auth.split(":");
|
|
proxy.username = username;
|
|
proxy.password = password;
|
|
}
|
|
|
|
for (const addon of addons.split("&")) {
|
|
let [key, value] = addon.split("=");
|
|
key = key.replace(/_/, "-");
|
|
value = decodeURIComponent(value);
|
|
switch (key) {
|
|
case "tls":
|
|
proxy.tls = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "fingerprint":
|
|
proxy["fingerprint"] = value;
|
|
break;
|
|
case "skip-cert-verify":
|
|
proxy["skip-cert-verify"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "udp":
|
|
proxy["udp"] = /(TRUE)|1/i.test(value);
|
|
break;
|
|
case "ip-version":
|
|
if (
|
|
["dual", "ipv4", "ipv6", "ipv4-prefer", "ipv6-prefer"].includes(value)
|
|
) {
|
|
proxy["ip-version"] = value as
|
|
| "dual"
|
|
| "ipv4"
|
|
| "ipv6"
|
|
| "ipv4-prefer"
|
|
| "ipv6-prefer";
|
|
} else {
|
|
proxy["ip-version"] = "dual";
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return proxy;
|
|
}
|