80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
/**
|
|
* Convert proxy URIs (ss://, vmess://, trojan://) to Clash YAML flow-style proxy lines.
|
|
* Returns a string like ` - {name: "NodeName", type: ss, server: ..., port: ..., ...}`
|
|
* or null if the URI is unsupported/invalid.
|
|
*/
|
|
export function uriToClashLine(uri: string): string | null {
|
|
try {
|
|
if (uri.startsWith('ss://')) return ssToClash(uri);
|
|
if (uri.startsWith('vmess://')) return vmessToClash(uri);
|
|
if (uri.startsWith('trojan://')) return trojanToClash(uri);
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Escape a string value for use inside double-quoted YAML flow style */
|
|
function esc(s: string): string {
|
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
}
|
|
|
|
function ssToClash(uri: string): string | null {
|
|
const url = new URL(uri);
|
|
const name = decodeURIComponent(url.hash.slice(1));
|
|
const server = url.hostname;
|
|
const port = url.port;
|
|
|
|
// userinfo is base64(method:password)
|
|
const decoded = Buffer.from(url.username, 'base64').toString();
|
|
const firstColon = decoded.indexOf(':');
|
|
if (firstColon === -1) return null;
|
|
const cipher = decoded.slice(0, firstColon);
|
|
const password = decoded.slice(firstColon + 1);
|
|
|
|
return ` - {name: "${esc(name)}", type: ss, server: ${server}, port: ${port}, cipher: ${cipher}, password: "${esc(password)}"}`;
|
|
}
|
|
|
|
function vmessToClash(uri: string): string | null {
|
|
const b64 = uri.replace('vmess://', '');
|
|
const json = JSON.parse(Buffer.from(b64, 'base64').toString());
|
|
|
|
const name = json.ps || 'VMess';
|
|
const server = json.add;
|
|
const port = json.port;
|
|
const uuid = json.id;
|
|
|
|
if (!server || !port || !uuid) return null;
|
|
|
|
let line = ` - {name: "${esc(name)}", type: vmess, server: ${server}, port: ${port}, uuid: ${uuid}, alterId: 0, cipher: auto`;
|
|
|
|
if (json.tls === 'tls') {
|
|
line += ', tls: true';
|
|
if (json.sni) line += `, sni: ${json.sni}`;
|
|
}
|
|
|
|
if (json.net === 'ws') {
|
|
line += ', network: ws';
|
|
const wsOpts: string[] = [];
|
|
if (json.path) wsOpts.push(`path: "${esc(json.path)}"`);
|
|
if (json.host) wsOpts.push(`headers: {Host: ${json.host}}`);
|
|
if (wsOpts.length > 0) {
|
|
line += `, ws-opts: {${wsOpts.join(', ')}}`;
|
|
}
|
|
}
|
|
|
|
line += '}';
|
|
return line;
|
|
}
|
|
|
|
function trojanToClash(uri: string): string | null {
|
|
const url = new URL(uri);
|
|
const name = decodeURIComponent(url.hash.slice(1));
|
|
const server = url.hostname;
|
|
const port = url.port;
|
|
const password = url.username;
|
|
const sni = url.searchParams.get('sni') || server;
|
|
|
|
return ` - {name: "${esc(name)}", type: trojan, server: ${server}, port: ${port}, password: "${esc(password)}", sni: ${sni}}`;
|
|
}
|