UNPKG

@herbertgao/surgio

Version:

Generating rules for Surge, Clash, Quantumult like a PRO

476 lines 21.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseClashConfig = exports.getClashSubscription = void 0; const assert_1 = __importDefault(require("assert")); const yaml_1 = __importDefault(require("yaml")); const lodash_1 = __importDefault(require("lodash")); const logger_1 = require("@surgio/logger"); const zod_1 = require("zod"); const constant_1 = require("../constant"); const types_1 = require("../types"); const utils_1 = require("../utils"); const relayable_url_1 = __importDefault(require("../utils/relayable-url")); const validators_1 = require("../validators"); const Provider_1 = __importDefault(require("./Provider")); const logger = (0, logger_1.createLogger)({ service: 'surgio:ClashProvider', }); class ClashProvider extends Provider_1.default { #originalUrl; udpRelay; tls13; constructor(name, config) { super(name, config); const schema = zod_1.z.object({ url: zod_1.z.string().url(), udpRelay: zod_1.z.boolean().optional(), tls13: zod_1.z.boolean().optional(), }); const result = schema.safeParse(config); // istanbul ignore next if (!result.success) { throw new utils_1.SurgioError('ClashProvider 配置校验失败', { cause: result.error, providerName: name, }); } this.#originalUrl = result.data.url; this.udpRelay = result.data.udpRelay; this.tls13 = result.data.tls13; this.supportGetSubscriptionUserInfo = true; } // istanbul ignore next get url() { return (0, relayable_url_1.default)(this.#originalUrl, this.config.relayUrl); } getSubscriptionUserInfo = async (params = {}) => { const requestUserAgent = this.determineRequestUserAgent(params.requestUserAgent); const { subscriptionUserinfo } = await (0, exports.getClashSubscription)({ url: this.url, udpRelay: this.udpRelay, tls13: this.tls13, requestUserAgent, }); if (subscriptionUserinfo) { return subscriptionUserinfo; } return undefined; }; getNodeList = async (params = {}) => { const requestUserAgent = this.determineRequestUserAgent(params.requestUserAgent); const { nodeList } = await (0, exports.getClashSubscription)({ url: this.url, udpRelay: this.udpRelay, tls13: this.tls13, requestUserAgent, }); if (this.config.hooks?.afterNodeListResponse) { const newList = await this.config.hooks.afterNodeListResponse(nodeList, params); if (newList) { return newList; } } return nodeList; }; } exports.default = ClashProvider; const getClashSubscription = async ({ url, udpRelay, tls13, requestUserAgent, }) => { (0, assert_1.default)(url, '未指定订阅地址 url'); const response = await Provider_1.default.requestCacheableResource(url, { requestUserAgent: requestUserAgent || (0, utils_1.getNetworkClashUA)(), }); let clashConfig; try { // eslint-disable-next-line prefer-const clashConfig = yaml_1.default.parse(response.body); } catch (err) /* istanbul ignore next */ { throw new Error(`${url} 不是一个合法的 YAML 文件`); } if (!lodash_1.default.isPlainObject(clashConfig) || (!('Proxy' in clashConfig) && !('proxies' in clashConfig))) { throw new Error(`${url} 订阅内容有误,请检查后重试`); } const proxyList = clashConfig.Proxy || clashConfig.proxies; // istanbul ignore next if (!Array.isArray(proxyList)) { throw new Error(`${url} 订阅内容有误,请检查后重试`); } return { nodeList: (0, exports.parseClashConfig)(proxyList, udpRelay, tls13), subscriptionUserinfo: response.subscriptionUserinfo, }; }; exports.getClashSubscription = getClashSubscription; const parseClashConfig = (proxyList, udpRelay, tls13) => { const nodeList = proxyList.map((item) => { switch (item.type) { case 'ss': { // istanbul ignore next if (item.plugin && !['obfs', 'v2ray-plugin'].includes(item.plugin)) { logger.warn(`不支持从 Clash 订阅中读取 ${item.plugin} 类型的 Shadowsocks 节点,节点 ${item.name} 会被省略`); return undefined; } // istanbul ignore next if (item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode.toLowerCase() === 'quic') { logger.warn(`不支持从 Clash 订阅中读取 QUIC 模式的 Shadowsocks 节点,节点 ${item.name} 会被省略`); return undefined; } const wsHeaders = (0, utils_1.lowercaseHeaderKeys)(lodash_1.default.get(item, 'plugin-opts.headers', {})); return { type: types_1.NodeTypeEnum.Shadowsocks, nodeName: item.name, hostname: item.server, port: item.port, method: item.cipher, password: item.password, udpRelay: resolveUdpRelay(item.udp, udpRelay), // obfs-local 新格式 ...(item.plugin && item.plugin === 'obfs' ? { obfs: item['plugin-opts'].mode, obfsHost: item['plugin-opts'].host || 'www.bing.com', } : null), // obfs-local 旧格式 ...(item.obfs ? { obfs: item.obfs, obfsHost: item['obfs-host'] || 'www.bing.com', } : null), // v2ray-plugin ...(item.plugin && item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode === 'websocket' ? { obfs: item['plugin-opts'].tls === true ? 'wss' : 'ws', obfsHost: item['plugin-opts'].host || item.server, obfsUri: item['plugin-opts'].path || '/', wsHeaders, ...(item['plugin-opts'].tls === true ? { skipCertVerify: item['plugin-opts']['skip-cert-verify'] === true, tls13: tls13 ?? false, } : null), ...(typeof item['plugin-opts'].mux === 'boolean' ? { mux: item['plugin-opts'].mux, } : null), } : null), }; } case 'vless': case 'vmess': { // istanbul ignore next if (item.network && ![ ...constant_1.CLASH_META_SUPPORTED_VMESS_NETWORK, ...constant_1.STASH_SUPPORTED_VMESS_NETWORK, ].includes(item.network)) { logger.warn(`不支持从 Clash 订阅中读取 network 类型为 ${item.network} 的 Vmess 节点,节点 ${item.name} 会被省略`); return undefined; } const nodeType = item.type === 'vless' ? types_1.NodeTypeEnum.Vless : types_1.NodeTypeEnum.Vmess; const fallbackCipherMethod = item.type === 'vless' ? 'none' : 'auto'; const vmessNode = { type: nodeType, nodeName: item.name, hostname: item.server, port: item.port, uuid: item.uuid, method: item.cipher || fallbackCipherMethod, udpRelay: resolveUdpRelay(item.udp, udpRelay), network: item.network || 'tcp', }; if (vmessNode.type === types_1.NodeTypeEnum.Vmess) { vmessNode.tls = item.tls === true; vmessNode.alterId = item.alterId ? `${item.alterId}` : '0'; } if (vmessNode.type === types_1.NodeTypeEnum.Vless && item.tls !== true) { logger.warn(`未经 TLS 传输的 VLESS 协议不安全并且不被 Surgio 支持,节点 ${item.name} 会被省略`); return undefined; } if ((vmessNode.type === types_1.NodeTypeEnum.Vmess && vmessNode.tls) || vmessNode.type === types_1.NodeTypeEnum.Vless) { if (typeof item.servername === 'string') { vmessNode.sni = item.servername; } if (typeof item.sni === 'string') { vmessNode.sni = item.sni; } if (typeof item['client-fingerprint'] === 'string') { vmessNode.clientFingerprint = item['client-fingerprint']; } vmessNode.skipCertVerify = item['skip-cert-verify'] === true; vmessNode.tls13 = tls13 === true; } if (vmessNode.type === types_1.NodeTypeEnum.Vless) { vmessNode.flow = item.flow; if (item['reality-opts']) { vmessNode.realityOpts = { publicKey: item['reality-opts']['public-key'], shortId: item['reality-opts']['short-id'], spiderX: item['reality-opts']['spider-x'], }; if (!vmessNode.clientFingerprint) { logger.warn(`VLESS + Reality 协议需要设置 clientFingerprint 字段,节点 ${item.name} 会被省略`); return undefined; } } } switch (vmessNode.network) { case 'ws': vmessNode.wsOpts = item['ws-opts'] || { path: '/', }; if (item['ws-path']) { vmessNode.wsOpts.path = item['ws-path']; } if (item['ws-headers']) { vmessNode.wsOpts.headers = item['ws-headers']; } break; case 'h2': vmessNode.h2Opts = item['h2-opts']; break; case 'http': vmessNode.httpOpts = { ...item['http-opts'], headers: resolveVmessHttpHeaders(item['http-opts'].headers) || {}, }; break; case 'grpc': vmessNode.grpcOpts = { serviceName: item['grpc-opts']['grpc-service-name'], }; break; } return vmessNode; } case 'http': if (!item.tls) { return { type: types_1.NodeTypeEnum.HTTP, nodeName: item.name, hostname: item.server, port: item.port, username: item.username /* istanbul ignore next */ || '', password: item.password /* istanbul ignore next */ || '', }; } return { type: types_1.NodeTypeEnum.HTTPS, nodeName: item.name, hostname: item.server, port: item.port, username: item.username || '', password: item.password || '', tls13: tls13 ?? false, skipCertVerify: item['skip-cert-verify'] === true, }; case 'snell': return { type: types_1.NodeTypeEnum.Snell, nodeName: item.name, hostname: item.server, port: item.port, psk: item.psk, obfs: lodash_1.default.get(item, 'obfs-opts.mode', 'http'), ...(typeof item?.['obfs-opts']?.host !== 'undefined' ? { obfsHost: item['obfs-opts'].host } : null), ...('version' in item ? { version: item.version } : null), }; // istanbul ignore next case 'ssr': return { type: types_1.NodeTypeEnum.Shadowsocksr, nodeName: item.name, hostname: item.server, port: item.port, password: item.password, obfs: item.obfs, obfsparam: item['obfs-param'] ?? item.obfsparam, protocol: item.protocol, protoparam: item['protocol-param'] ?? item.protocolparam, method: item.cipher, udpRelay: resolveUdpRelay(item.udp, udpRelay), }; case 'trojan': { const network = item.network; const wsOpts = lodash_1.default.get(item, 'ws-opts', {}); const wsHeaders = (0, utils_1.lowercaseHeaderKeys)(lodash_1.default.get(wsOpts, 'headers', {})); return { type: types_1.NodeTypeEnum.Trojan, nodeName: item.name, hostname: item.server, port: item.port, password: item.password, ...('skip-cert-verify' in item ? { skipCertVerify: item['skip-cert-verify'] === true } : null), ...('alpn' in item ? { alpn: item.alpn } : null), ...('sni' in item ? { sni: item.sni } : null), udpRelay: resolveUdpRelay(item.udp, udpRelay), tls13: tls13 ?? false, ...(network === 'ws' ? { network: 'ws', wsPath: lodash_1.default.get(wsOpts, 'path', '/'), wsHeaders } : null), }; } case 'tuic': { let input; if (item.version >= 5) { input = { type: types_1.NodeTypeEnum.Tuic, version: item.version, nodeName: item.name, hostname: item.server, port: item.port, password: item.password, uuid: item.uuid, ...('skip-cert-verify' in item ? { skipCertVerify: item['skip-cert-verify'] === true } : null), tls13: tls13 ?? false, ...('sni' in item ? { sni: item.sni } : null), ...('alpn' in item ? { alpn: item.alpn } : null), ...('ports' in item ? { portHopping: item.ports, } : null), ...('hop-interval' in item ? { portHoppingInterval: item['hop-interval'] } : null), }; } else { input = { type: types_1.NodeTypeEnum.Tuic, nodeName: item.name, hostname: item.server, port: item.port, token: item.token, ...('skip-cert-verify' in item ? { skipCertVerify: item['skip-cert-verify'] === true } : null), tls13: tls13 ?? false, ...('sni' in item ? { sni: item.sni } : null), ...('alpn' in item ? { alpn: item.alpn } : null), ...('ports' in item ? { portHopping: item.ports, } : null), ...('hop-interval' in item ? { portHoppingInterval: item['hop-interval'] } : null), }; } const result = validators_1.TuicNodeConfigValidator.safeParse(input); // istanbul ignore next if (!result.success) { throw new utils_1.SurgioError('Tuic 节点配置校验失败', { cause: result.error, }); } return result.data; } case 'hysteria2': { // istanbul ignore next if (item.obfs && item.obfs !== 'salamander') { throw new Error('不支持从 Clash 订阅中读取 Hysteria2 节点,因为其 obfs 不是 salamander'); } const input = { type: types_1.NodeTypeEnum.Hysteria2, nodeName: item.name, hostname: item.server, port: item.port, password: item.auth || item.password, ...(item.down ? { downloadBandwidth: (0, utils_1.parseBitrate)(item.down) } : null), ...(item.up ? { uploadBandwidth: (0, utils_1.parseBitrate)(item.up) } : null), ...(item.obfs ? { obfs: item.obfs } : null), ...(item['obfs-password'] ? { obfsPassword: item['obfs-password'] } : null), ...(item.sni ? { sni: item.sni } : null), ...('alpn' in item ? { alpn: item.alpn } : null), ...('skip-cert-verify' in item ? { skipCertVerify: item['skip-cert-verify'] === true } : null), ...('ports' in item ? { portHopping: item.ports, } : null), ...('hop-interval' in item ? { portHoppingInterval: item['hop-interval'] } : null), }; const result = validators_1.Hysteria2NodeConfigValidator.safeParse(input); // istanbul ignore next if (!result.success) { throw new utils_1.SurgioError('Hysteria2 节点配置校验失败', { cause: result.error, }); } return result.data; } case 'socks5': { const socks5Node = { type: types_1.NodeTypeEnum.Socks5, nodeName: item.name, hostname: item.server, port: item.port, }; if (item.username) { socks5Node.username = item.username; } if (item.password) { socks5Node.password = item.password; } if (item.udp) { socks5Node.udpRelay = item.udp; } if (item.tls) { socks5Node.tls = item.tls; } if (item['skip-cert-verify']) { socks5Node.skipCertVerify = item['skip-cert-verify']; } return socks5Node; } default: logger.warn(`不支持从 Clash 订阅中读取 ${item.type} 的节点,节点 ${item.name} 会被省略`); return undefined; } }); return nodeList.filter((item) => item !== undefined); }; exports.parseClashConfig = parseClashConfig; function resolveUdpRelay(val, defaultVal = false) { if (val !== void 0) { return val; } return defaultVal; } function resolveVmessHttpHeaders(headers) { return Object.keys(headers).reduce((acc, key) => { if (headers[key].length) { acc[key] = headers[key][0]; } return acc; }, {}); } //# sourceMappingURL=ClashProvider.js.map