@herbertgao/surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
407 lines • 15.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSurgeNodeNames = exports.getSurgeWireguardNodes = exports.getSurgeNodes = exports.getSurgeExtendHeaders = void 0;
const logger_1 = require("@surgio/logger");
const constant_1 = require("../constant");
const types_1 = require("../types");
const filters_1 = require("../filters");
const _1 = require("./");
const logger = (0, logger_1.createLogger)({ service: 'surgio:utils:surge' });
const getSurgeExtendHeaders = (headers) => {
return Object.keys(headers)
.map((headerKey) => `${headerKey.toLowerCase()}:${headers[headerKey]}`)
.join('|');
};
exports.getSurgeExtendHeaders = getSurgeExtendHeaders;
/**
* @see https://manual.nssurge.com/policy/proxy.html
*/
const getSurgeNodes = function (nodeList, filter) {
const result = (0, filters_1.applyFilter)(nodeList, filter)
.map((nodeConfig) => {
const result = nodeListMapper(nodeConfig);
if (!result) {
return undefined;
}
const [nodeName, nodeConfigString] = result;
return [nodeName, appendCommonConfig(nodeConfigString, nodeConfig)];
})
.filter((item) => item !== undefined)
.map((item) => item[1]);
return result.join('\n');
};
exports.getSurgeNodes = getSurgeNodes;
const getSurgeWireguardNodes = (nodeList) => {
const result = nodeList
.map((nodeConfig) => {
if (nodeConfig.type !== types_1.NodeTypeEnum.Wireguard ||
nodeConfig.enable === false) {
return undefined;
}
const nodeConfigSection = [
`[WireGuard ${nodeConfig.nodeName}]`,
`self-ip=${nodeConfig.selfIp}`,
`private-key=${nodeConfig.privateKey}`,
];
const optionalKeys = [
'mtu',
'preferIpv6',
'selfIpV6',
];
for (const key of optionalKeys) {
if (nodeConfig[key] !== undefined) {
nodeConfigSection.push(...(0, _1.pickAndFormatStringList)(nodeConfig, [key], {
keyFormat: 'kebabCase',
}));
}
}
if (nodeConfig.dnsServers) {
nodeConfigSection.push(`dns-server=${JSON.stringify(nodeConfig.dnsServers.join(', '))}`);
}
const peerList = [];
for (const peer of nodeConfig.peers) {
const peerConfig = [
`endpoint=${peer.endpoint}`,
`public-key=${JSON.stringify(peer.publicKey)}`,
];
const optionalPeerConfigKeys = [
'presharedKey',
'allowedIps',
'keepalive',
];
for (const key of optionalPeerConfigKeys) {
if (peer[key] !== undefined) {
peerConfig.push(...(0, _1.pickAndFormatStringList)(peer, [key], {
keyFormat: 'kebabCase',
stringifyValue: true,
}));
}
}
// istanbul ignore next
if (peer.reservedBits) {
peerConfig.push(`client-id=${peer.reservedBits.join('/')}`);
}
peerList.push(`(${peerConfig.join(', ')})`);
}
nodeConfigSection.push(`peer=${peerList.join(', ')}`);
return nodeConfigSection.join('\n');
})
.filter((item) => item !== undefined);
return result.join('\n\n');
};
exports.getSurgeWireguardNodes = getSurgeWireguardNodes;
const getSurgeNodeNames = function (nodeList, filter) {
const result = (0, filters_1.applyFilter)(nodeList, filter)
.map(nodeListMapper)
.filter((item) => item !== undefined)
.map((item) => item[0]);
return result.join(', ');
};
exports.getSurgeNodeNames = getSurgeNodeNames;
function nodeListMapper(nodeConfig) {
switch (nodeConfig.type) {
case types_1.NodeTypeEnum.Shadowsocks: {
if (nodeConfig.obfs && ['ws', 'wss'].includes(nodeConfig.obfs)) {
logger.warn(`不支持为 Surge 生成 v2ray-plugin 的 Shadowsocks 节点,节点 ${nodeConfig.nodeName} 会被省略`);
return void 0;
}
return [
nodeConfig.nodeName,
[
nodeConfig.nodeName,
[
'ss',
nodeConfig.hostname,
nodeConfig.port,
'encrypt-method=' + nodeConfig.method,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['password', 'udpRelay', 'obfs', 'obfsHost'], {
keyFormat: 'kebabCase',
}),
].join(', '),
].join(' = '),
];
}
case types_1.NodeTypeEnum.HTTPS: {
return [
nodeConfig.nodeName,
[
nodeConfig.nodeName,
[
'https',
nodeConfig.hostname,
nodeConfig.port,
nodeConfig.username /* istanbul ignore next */ || '',
nodeConfig.password /* istanbul ignore next */ || '',
].join(', '),
].join(' = '),
];
}
case types_1.NodeTypeEnum.HTTP: {
return [
nodeConfig.nodeName,
[
nodeConfig.nodeName,
[
'http',
nodeConfig.hostname,
nodeConfig.port,
nodeConfig.username /* istanbul ignore next */ || '',
nodeConfig.password /* istanbul ignore next */ || '',
].join(', '),
].join(' = '),
];
}
case types_1.NodeTypeEnum.Snell: {
return [
nodeConfig.nodeName,
[
nodeConfig.nodeName,
[
'snell',
nodeConfig.hostname,
nodeConfig.port,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['psk', 'obfs', 'obfsHost', 'version', 'reuse'], {
keyFormat: 'kebabCase',
}),
].join(', '),
].join(' = '),
];
}
case types_1.NodeTypeEnum.Shadowsocksr: {
// istanbul ignore next
if (!nodeConfig.binPath) {
throw new Error('请按照文档 https://url.royli.dev/vdGh2 添加 Shadowsocksr 二进制文件路径');
}
const args = [
'-s',
nodeConfig.hostname,
'-p',
`${nodeConfig.port}`,
'-m',
nodeConfig.method,
'-o',
nodeConfig.obfs,
'-O',
nodeConfig.protocol,
'-k',
nodeConfig.password,
'-l',
`${nodeConfig.localPort}`,
'-b',
'127.0.0.1',
];
if (nodeConfig.protoparam) {
args.push('-G', nodeConfig.protoparam);
}
if (nodeConfig.obfsparam) {
args.push('-g', nodeConfig.obfsparam);
}
const nodeConfigString = [
'external',
`exec = ${JSON.stringify(nodeConfig.binPath)}`,
...args.map((arg) => `args = ${JSON.stringify(arg)}`),
`local-port = ${nodeConfig.localPort}`,
];
if (nodeConfig.localPort === 0) {
throw new Error(`为 Surge 生成 SSR 配置时必须为 Provider ${nodeConfig.provider?.name} 设置 startPort,参考 https://url.royli.dev/bWcpe`);
}
if (nodeConfig?.surgeConfig?.resolveHostname &&
nodeConfig.hostnameIp &&
nodeConfig.hostnameIp.length) {
nodeConfigString.push(...nodeConfig.hostnameIp.map((item) => `addresses = ${item}`));
}
if ((0, _1.isIp)(nodeConfig.hostname)) {
nodeConfigString.push(`addresses = ${nodeConfig.hostname}`);
}
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, nodeConfigString.join(', ')].join(' = '),
];
}
case types_1.NodeTypeEnum.Vmess: {
if (!constant_1.SURGE_SUPPORTED_VMESS_NETWORK.includes(nodeConfig.network)) {
logger.warn(`Surge 不支持 Vmess ${nodeConfig.network} 节点,节点 ${nodeConfig.nodeName} 会被省略`);
return undefined;
}
const result = [
'vmess',
nodeConfig.hostname,
nodeConfig.port,
`username=${nodeConfig.uuid}`,
];
if (['chacha20-poly1305', 'aes-128-gcm'].includes(nodeConfig.method)) {
if (nodeConfig.method === 'chacha20-poly1305') {
result.push(`encrypt-method=chacha20-ietf-poly1305`);
}
else {
result.push(`encrypt-method=${nodeConfig.method}`);
}
}
if (nodeConfig.network === 'ws') {
result.push('ws=true');
result.push(`ws-path=${nodeConfig.wsOpts?.path || '/'}`);
result.push('ws-headers=' +
JSON.stringify((0, exports.getSurgeExtendHeaders)({
'user-agent': constant_1.OBFS_UA,
...nodeConfig.wsOpts?.headers,
})));
}
if (nodeConfig.tls) {
result.push('tls=true');
}
if (nodeConfig?.surgeConfig?.vmessAEAD) {
result.push('vmess-aead=true');
}
else {
result.push('vmess-aead=false');
}
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, result.join(', ')].join(' = '),
];
}
case types_1.NodeTypeEnum.Trojan: {
const result = [
'trojan',
nodeConfig.hostname,
`${nodeConfig.port}`,
`password=${nodeConfig.password}`,
];
if (nodeConfig.network === 'ws') {
result.push('ws=true');
result.push(`ws-path=${nodeConfig.wsPath}`);
if (nodeConfig.wsHeaders) {
result.push('ws-headers=' +
JSON.stringify((0, exports.getSurgeExtendHeaders)(nodeConfig.wsHeaders)));
}
}
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, result.join(', ')].join(' = '),
];
}
case types_1.NodeTypeEnum.Socks5: {
const result = [
nodeConfig.tls === true ? 'socks5-tls' : 'socks5',
nodeConfig.hostname,
nodeConfig.port,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['username', 'password', 'udpRelay'], {
keyFormat: 'kebabCase',
}),
];
if (nodeConfig.tls === true) {
result.push(...(typeof nodeConfig.clientCert === 'string'
? [`client-cert=${nodeConfig.clientCert}`]
: []));
}
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, result.join(', ')].join(' = '),
];
}
case types_1.NodeTypeEnum.Tuic: {
if ('version' in nodeConfig && Number(nodeConfig.version) === 5) {
const result = [
'tuic-v5',
nodeConfig.hostname,
nodeConfig.port,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['password', 'uuid'], {
keyFormat: 'kebabCase',
}),
];
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, result.join(', ')].join(' = '),
];
}
const result = [
'tuic',
nodeConfig.hostname,
nodeConfig.port,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['token'], {
keyFormat: 'kebabCase',
}),
];
return [
nodeConfig.nodeName,
[nodeConfig.nodeName, result.join(', ')].join(' = '),
];
}
case types_1.NodeTypeEnum.Hysteria2:
// istanbul ignore next
if (nodeConfig.uploadBandwidth) {
logger.info(`Surge 不支持为 Hysteria2 节点配置 uploadBandwidth,节点 ${nodeConfig.nodeName} 将不包含此字段`);
}
// istanbul ignore next
if (nodeConfig.obfs) {
logger.warn(`Surge 不支持为 Hysteria2 节点配置 obfs,节点 ${nodeConfig.nodeName} 将被忽略`);
return undefined;
}
return [
nodeConfig.nodeName,
[
`${nodeConfig.nodeName} = hysteria2`,
nodeConfig.hostname,
nodeConfig.port,
...(0, _1.pickAndFormatStringList)(nodeConfig, ['password', 'downloadBandwidth'], {
keyFormat: 'kebabCase',
}),
].join(', '),
];
case types_1.NodeTypeEnum.Wireguard:
logger.info(`请配合使用 getSurgeWireguardNodes 生成 ${nodeConfig.nodeName} 节点配置`);
return [
nodeConfig.nodeName,
[
`${nodeConfig.nodeName} = wireguard`,
`section-name = ${nodeConfig.nodeName}`,
].join(', '),
];
// istanbul ignore next
default:
logger.warn(`不支持为 Surge 生成 ${nodeConfig.type} 的节点,节点 ${nodeConfig.nodeName} 会被省略`);
return undefined;
}
}
function appendCommonConfig(original, nodeConfig) {
const appendConfig = [
...(0, _1.pickAndFormatStringList)(nodeConfig, [
'tfo',
'mptcp',
'ecn',
'underlyingProxy',
'testUrl',
'testTimeout',
'tls13',
'skipCertVerify',
'sni',
'serverCertFingerprintSha256',
'blockQuic',
'portHopping',
'portHoppingInterval',
], {
keyFormat: 'kebabCase',
}),
...parseShadowTlsConfig(nodeConfig),
];
if (nodeConfig.type === types_1.NodeTypeEnum.Tuic) {
appendConfig.push(...('alpn' in nodeConfig && Array.isArray(nodeConfig.alpn)
? [`alpn=${nodeConfig.alpn.join(',')}`]
: []));
}
if (!appendConfig.length) {
return original;
}
return original + ', ' + appendConfig.join(', ');
}
function parseShadowTlsConfig(nodeConfig) {
const result = [];
if (nodeConfig.shadowTls) {
result.push(`shadow-tls-password=${nodeConfig.shadowTls.password}`, `shadow-tls-sni=${nodeConfig.shadowTls.sni}`);
if (nodeConfig.shadowTls.version) {
result.push(`shadow-tls-version=${nodeConfig.shadowTls.version}`);
}
}
return result;
}
//# sourceMappingURL=surge.js.map