UNPKG

@herbertgao/surgio

Version:

Generating rules for Surge, Clash, Quantumult like a PRO

529 lines 20.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getHeader = exports.parseBitrate = exports.getHostnameFromHost = exports.getPortFromHost = exports.checkNotNullish = exports.isGFWFree = exports.isFlyIO = exports.isAWS = exports.isAWSLambda = exports.isNetlify = exports.isRailway = exports.isGitLabCI = exports.isGitHubActions = exports.isHeroku = exports.isVercel = exports.isNow = exports.isIp = exports.lowercaseHeaderKeys = exports.ensureConfigFolder = exports.decodeStringList = exports.pickAndFormatKeys = exports.pickAndFormatStringList = exports.changeCase = exports.getNodeNames = exports.getShadowsocksNodesJSON = exports.getV2rayNNodes = exports.getShadowsocksrNodes = exports.getShadowsocksNodes = exports.toMD5 = exports.fromBase64 = exports.toBase64 = exports.fromUrlSafeBase64 = exports.toUrlSafeBase64 = exports.getUrl = exports.getDownloadUrl = exports.httpClient = void 0; const os_1 = __importDefault(require("os")); const path_1 = require("path"); const url_1 = require("url"); const net_1 = __importDefault(require("net")); const crypto_1 = __importDefault(require("crypto")); const urlsafe_base64_1 = __importDefault(require("urlsafe-base64")); const query_string_1 = __importDefault(require("query-string")); const fs_extra_1 = __importDefault(require("fs-extra")); const logger_1 = require("@surgio/logger"); const change_case_1 = require("change-case"); const types_1 = require("../types"); const constant_1 = require("../constant"); const filters_1 = require("../filters"); const env_flag_1 = require("./env-flag"); __exportStar(require("./surge"), exports); __exportStar(require("./surfboard"), exports); __exportStar(require("./clash"), exports); __exportStar(require("./singbox"), exports); __exportStar(require("./quantumult"), exports); __exportStar(require("./loon"), exports); __exportStar(require("./remote-snippet"), exports); __exportStar(require("./subscription"), exports); __exportStar(require("./time"), exports); __exportStar(require("./errors"), exports); __exportStar(require("./env-flag"), exports); var http_client_1 = require("./http-client"); Object.defineProperty(exports, "httpClient", { enumerable: true, get: function () { return __importDefault(http_client_1).default; } }); const logger = (0, logger_1.createLogger)({ service: 'surgio:utils' }); const getDownloadUrl = (baseUrl = '/', artifactName, inline = true, accessToken) => { let urlSearchParams; let name; if (artifactName.includes('?')) { urlSearchParams = new url_1.URLSearchParams(artifactName.split('?')[1]); name = artifactName.split('?')[0]; } else { urlSearchParams = new url_1.URLSearchParams(); name = artifactName; } if (accessToken) { urlSearchParams.set('access_token', accessToken); } if (!inline) { urlSearchParams.set('dl', '1'); } const query = urlSearchParams.toString(); return `${baseUrl}${name}${query ? '?' + query : ''}`; }; exports.getDownloadUrl = getDownloadUrl; const getUrl = (baseUrl, path, accessToken) => { path = path.replace(/^\//, ''); const url = new url_1.URL(path, baseUrl); if (accessToken) { url.searchParams.set('access_token', accessToken); } return url.toString(); }; exports.getUrl = getUrl; // istanbul ignore next const toUrlSafeBase64 = (str) => urlsafe_base64_1.default.encode(Buffer.from(str, 'utf8')); exports.toUrlSafeBase64 = toUrlSafeBase64; // istanbul ignore next const fromUrlSafeBase64 = (str) => { if (urlsafe_base64_1.default.validate(str)) { return urlsafe_base64_1.default.decode(str).toString(); } return (0, exports.fromBase64)(str); }; exports.fromUrlSafeBase64 = fromUrlSafeBase64; // istanbul ignore next const toBase64 = (str) => Buffer.from(str, 'utf8').toString('base64'); exports.toBase64 = toBase64; // istanbul ignore next const fromBase64 = (str) => Buffer.from(str, 'base64').toString('utf8'); exports.fromBase64 = fromBase64; // istanbul ignore next const toMD5 = (str) => crypto_1.default.createHash('md5').update(str).digest('hex'); exports.toMD5 = toMD5; /** * @see https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme */ const getShadowsocksNodes = (list, groupName = 'Surgio') => { const result = list .map((nodeConfig) => { // istanbul ignore next if (nodeConfig.enable === false) { return null; } switch (nodeConfig.type) { case types_1.NodeTypeEnum.Shadowsocks: { const query = { ...(nodeConfig.obfs ? { plugin: `${encodeURIComponent(`obfs-local;obfs=${nodeConfig.obfs};obfs-host=${nodeConfig.obfsHost}`)}`, } : null), ...(groupName ? { group: encodeURIComponent(groupName) } : null), }; return [ 'ss://', (0, exports.toUrlSafeBase64)(`${nodeConfig.method}:${nodeConfig.password}`), '@', nodeConfig.hostname, ':', nodeConfig.port, '/?', query_string_1.default.stringify(query, { encode: false, sort: false, }), '#', encodeURIComponent(nodeConfig.nodeName), ].join(''); } // istanbul ignore next default: logger.warn(`在生成 Shadowsocks 节点时出现了 ${nodeConfig.type} 节点,节点 ${nodeConfig.nodeName} 会被省略`); return null; } }) .filter((item) => !!item); return result.join('\n'); }; exports.getShadowsocksNodes = getShadowsocksNodes; const getShadowsocksrNodes = (list, groupName) => { const result = list .map((nodeConfig) => { // istanbul ignore next if (nodeConfig.enable === false) { return void 0; } switch (nodeConfig.type) { case types_1.NodeTypeEnum.Shadowsocksr: { const baseUri = [ nodeConfig.hostname, nodeConfig.port, nodeConfig.protocol, nodeConfig.method, nodeConfig.obfs, (0, exports.toUrlSafeBase64)(nodeConfig.password), ].join(':'); const query = { obfsparam: (0, exports.toUrlSafeBase64)(nodeConfig.obfsparam), protoparam: (0, exports.toUrlSafeBase64)(nodeConfig.protoparam), remarks: (0, exports.toUrlSafeBase64)(nodeConfig.nodeName), group: (0, exports.toUrlSafeBase64)(groupName), udpport: 0, uot: 0, }; return ('ssr://' + (0, exports.toUrlSafeBase64)([ baseUri, '/?', query_string_1.default.stringify(query, { encode: false, }), ].join(''))); } // istanbul ignore next default: logger.warn(`在生成 Shadowsocksr 节点时出现了 ${nodeConfig.type} 节点,节点 ${nodeConfig.nodeName} 会被省略`); return void 0; } }) .filter((item) => item !== undefined); return result.join('\n'); }; exports.getShadowsocksrNodes = getShadowsocksrNodes; // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) const getV2rayNNodes = (list) => { const result = list .map((nodeConfig) => { // istanbul ignore next if (nodeConfig.enable === false) { return void 0; } if (!constant_1.V2RAYN_SUPPORTED_VMESS_NETWORK.includes(nodeConfig.network)) { logger.warn(`在生成 V2Ray 节点时出现了不被支持的 ${nodeConfig.network} 协议,节点 ${nodeConfig.nodeName} 会被省略`); return void 0; } switch (nodeConfig.type) { case types_1.NodeTypeEnum.Vmess: { const json = { v: '2', ps: nodeConfig.nodeName, add: nodeConfig.hostname, port: `${nodeConfig.port}`, id: nodeConfig.uuid, aid: `${nodeConfig.alterId}` || '0', scy: nodeConfig.method, net: nodeConfig.network === 'http' ? 'tcp' : nodeConfig.network, type: nodeConfig.network === 'http' ? 'http' : 'none', }; if (nodeConfig.tls) { json.tls = 'tls'; if (nodeConfig.sni) { json.sni = nodeConfig.sni; } if (nodeConfig.alpn) { json.alpn = nodeConfig.alpn.join(','); } } switch (nodeConfig.network) { case 'ws': if (nodeConfig.wsOpts) { const obfsHost = (0, exports.getHeader)(nodeConfig.wsOpts.headers, 'host'); json.path = nodeConfig.wsOpts.path; if (obfsHost) { json.host = obfsHost; } } break; case 'http': if (nodeConfig.httpOpts) { const obfsHost = (0, exports.getHeader)(nodeConfig.httpOpts.headers, 'host'); json.path = nodeConfig.httpOpts.path[0]; if (obfsHost) { json.host = obfsHost; } } break; case 'h2': if (nodeConfig.h2Opts) { json.path = nodeConfig.h2Opts.path; json.host = nodeConfig.h2Opts.host[0]; } break; case 'grpc': if (nodeConfig.grpcOpts) { json.path = nodeConfig.grpcOpts.serviceName; } break; } return 'vmess://' + (0, exports.toBase64)(JSON.stringify(json)); } // istanbul ignore next default: logger.warn(`在生成 V2Ray 节点时出现了 ${nodeConfig.type} 节点,节点 ${nodeConfig.nodeName} 会被省略`); return void 0; } }) .filter((item) => item !== undefined); return result.join('\n'); }; exports.getV2rayNNodes = getV2rayNNodes; // istanbul ignore next const getShadowsocksNodesJSON = (list) => { const nodes = list .map((nodeConfig) => { // istanbul ignore next if (nodeConfig.enable === false) { return null; } switch (nodeConfig.type) { case types_1.NodeTypeEnum.Shadowsocks: { const useObfs = Boolean(nodeConfig.obfs && nodeConfig.obfsHost); return { remarks: nodeConfig.nodeName, server: nodeConfig.hostname, server_port: nodeConfig.port, method: nodeConfig.method, remarks_base64: (0, exports.toUrlSafeBase64)(nodeConfig.nodeName), password: nodeConfig.password, tcp_over_udp: false, udp_over_tcp: false, enable: true, ...(useObfs ? { plugin: 'obfs-local', 'plugin-opts': `obfs=${nodeConfig.obfs};obfs-host=${nodeConfig.obfsHost}`, } : null), }; } // istanbul ignore next default: logger.warn(`在生成 Shadowsocks 节点时出现了 ${nodeConfig.type} 节点,节点 ${nodeConfig.nodeName} 会被省略`); return undefined; } }) .filter((item) => item !== undefined); return JSON.stringify(nodes, null, 2); }; exports.getShadowsocksNodesJSON = getShadowsocksNodesJSON; const getNodeNames = function (list, filter, separator) { // istanbul ignore next if (arguments.length === 2 && typeof filter === 'undefined') { throw new Error(constant_1.ERR_INVALID_FILTER); } return (0, filters_1.applyFilter)(list, filter) .map((item) => item.nodeName) .join(separator || ', '); }; exports.getNodeNames = getNodeNames; // istanbul ignore next const changeCase = (str, format) => { switch (format) { case 'camelCase': return (0, change_case_1.camelCase)(str); case 'snakeCase': return (0, change_case_1.snakeCase)(str); case 'kebabCase': return (0, change_case_1.paramCase)(str); } }; exports.changeCase = changeCase; const pickAndFormatStringList = (obj, keyList, options = {}) => { const result = []; const { keyFormat, stringifyValue } = options; keyList.forEach((key) => { if (obj.hasOwnProperty(key) && obj[key] !== undefined) { const propertyKey = keyFormat ? (0, exports.changeCase)(key, keyFormat) : key; const propertyValue = obj[key]; if (Array.isArray(propertyValue)) { result.push(`${propertyKey}=${stringifyValue ? JSON.stringify(propertyValue.join(',')) : propertyValue.join(',')}`); } else { result.push(`${propertyKey}=${stringifyValue ? JSON.stringify(propertyValue) : propertyValue}`); } } }); return result; }; exports.pickAndFormatStringList = pickAndFormatStringList; /** * Pick and format keys from an object * Input: * { * foo: 'bar', * bAr: 'bar', * bAz: 'baz', * } * * pickAndFormatKeys(obj, ['foo', 'bar'], { keyFormat: 'kebabCase' }) * * Output: * { * 'foo': 'bar', * 'b-ar': 'bar', * 'b-az': 'baz', * } */ const pickAndFormatKeys = (obj, keyList, options = {}) => { const result = {}; keyList.forEach((key) => { if (obj.hasOwnProperty(key) && obj[key] !== undefined) { const propertyKey = options.keyFormat ? (0, exports.changeCase)(key, options.keyFormat) : key; result[propertyKey] = obj[key]; } }); return result; }; exports.pickAndFormatKeys = pickAndFormatKeys; const decodeStringList = (stringList) => { const result = {}; stringList.forEach((item) => { if (item.includes('=')) { const match = item.match(/^(.*?)=(.*?)$/); if (match) { const key = match[1].trim(); const value = match[2].trim(); result[key] = value || true; } } else { result[item.trim()] = true; } }); return result; }; exports.decodeStringList = decodeStringList; const ensureConfigFolder = (dir = os_1.default.homedir()) => { let baseDir; try { fs_extra_1.default.accessSync(dir, fs_extra_1.default.constants.W_OK); baseDir = dir; } catch (err) { // if the user do not have write permission // istanbul ignore next baseDir = '/tmp'; } const configDir = (0, path_1.join)(baseDir, '.config/surgio'); fs_extra_1.default.mkdirpSync(configDir); return configDir; }; exports.ensureConfigFolder = ensureConfigFolder; const lowercaseHeaderKeys = (headers) => { const wsHeaders = {}; Object.keys(headers).forEach((key) => { wsHeaders[key.toLowerCase()] = headers[key]; }); return wsHeaders; }; exports.lowercaseHeaderKeys = lowercaseHeaderKeys; // istanbul ignore next const isIp = (str) => net_1.default.isIPv4(str) || net_1.default.isIPv6(str); exports.isIp = isIp; // istanbul ignore next const isNow = () => typeof process.env.NOW_REGION !== 'undefined' || typeof process.env.VERCEL_REGION !== 'undefined'; exports.isNow = isNow; // istanbul ignore next const isVercel = () => (0, exports.isNow)(); exports.isVercel = isVercel; // istanbul ignore next const isHeroku = () => typeof process.env.DYNO !== 'undefined'; exports.isHeroku = isHeroku; // istanbul ignore next const isGitHubActions = () => typeof process.env.GITHUB_ACTIONS !== 'undefined'; exports.isGitHubActions = isGitHubActions; // istanbul ignore next const isGitLabCI = () => typeof process.env.GITLAB_CI !== 'undefined'; exports.isGitLabCI = isGitLabCI; // istanbul ignore next const isRailway = () => typeof process.env.RAILWAY_STATIC_URL !== 'undefined'; exports.isRailway = isRailway; // istanbul ignore next const isNetlify = () => typeof process.env.NETLIFY !== 'undefined'; exports.isNetlify = isNetlify; // istanbul ignore next const isAWSLambda = () => typeof process.env.AWS_LAMBDA_FUNCTION_NAME !== 'undefined'; exports.isAWSLambda = isAWSLambda; // istanbul ignore next const isAWS = () => (0, exports.isAWSLambda)() || typeof process.env.AWS_EXECUTION_ENV !== 'undefined' || typeof process.env.AWS_REGION !== 'undefined'; exports.isAWS = isAWS; // istanbul ignore next const isFlyIO = () => typeof process.env.FLY_REGION !== 'undefined'; exports.isFlyIO = isFlyIO; // istanbul ignore next const isGFWFree = () => (0, env_flag_1.getIsGFWFree)() || (0, exports.isAWS)() || (0, exports.isAWSLambda)() || (0, exports.isVercel)() || (0, exports.isHeroku)() || (0, exports.isGitHubActions)() || (0, exports.isGitLabCI)() || (0, exports.isRailway)() || (0, exports.isNetlify)() || (0, exports.isFlyIO)(); exports.isGFWFree = isGFWFree; const checkNotNullish = (val) => val !== null && val !== undefined; exports.checkNotNullish = checkNotNullish; const getPortFromHost = (host) => { const match = host.match(/:(\d+)$/); if (match) { return Number(match[1]); } throw new Error(`Invalid host: ${host}`); }; exports.getPortFromHost = getPortFromHost; const getHostnameFromHost = (host) => { const match = host.match(/^(.*?):/); if (match) { return match[1]; } throw new Error(`Invalid host: ${host}`); }; exports.getHostnameFromHost = getHostnameFromHost; /** * Returned value must be in Mbps * * Input value can be: * - 1.2 Mbps * - 1.2 * - 1200 * - 1200 Kbps * - 1.2 Gbps * Return value will be: * - 1.2 * - 1.2 * - 1.2 * - 1.2 * - 1200 */ const parseBitrate = (input) => { const inputStr = typeof input === 'number' ? `${input} Mbps` : input; const match = inputStr.match(/^(\d+(?:\.\d+)?)\s*(?:Mbps|Kbps|Gbps)?$/); if (!match) { throw new Error(`Invalid bitrate: ${inputStr}`); } const bitrate = Number(match[1]); if (inputStr.includes('Gbps')) { return bitrate * 1000; } else if (inputStr.includes('Kbps')) { return bitrate / 1000; } else { return bitrate; } }; exports.parseBitrate = parseBitrate; const getHeader = (headers, key) => { if (!headers) { return undefined; } const lowerCaseKey = key.toLowerCase(); const headerKey = Object.keys(headers).find((k) => k.toLowerCase() === lowerCaseKey); return headerKey ? headers[headerKey] : undefined; }; exports.getHeader = getHeader; //# sourceMappingURL=index.js.map