@herbertgao/surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
529 lines • 20.7 kB
JavaScript
;
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