@herbertgao/surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
370 lines • 16.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Artifact = void 0;
const events_1 = require("events");
const path_1 = __importDefault(require("path"));
const logger_1 = require("@surgio/logger");
const bluebird_1 = __importDefault(require("bluebird"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const lodash_1 = __importDefault(require("lodash"));
const provider_1 = require("../provider");
const types_1 = require("../types");
const utils_1 = require("../utils");
const dns_1 = require("../utils/dns");
const filters_1 = require("../filters");
const flag_1 = require("../utils/flag");
const validators_1 = require("../validators");
const template_1 = require("./template");
const json_template_1 = require("./json-template");
class Artifact extends events_1.EventEmitter {
surgioConfig;
options;
initProgress = 0;
artifact;
providerNameList;
nodeConfigListMap = new Map();
providerMap = new Map();
nodeList = [];
customFilters = {};
netflixFilter = filters_1.internalFilters.netflixFilter;
youtubePremiumFilter = filters_1.internalFilters.youtubePremiumFilter;
constructor(surgioConfig, artifactConfig, options = {}) {
super();
this.surgioConfig = surgioConfig;
this.options = options;
this.artifact = validators_1.ArtifactValidator.parse(artifactConfig);
const mainProviderName = this.artifact.provider;
const combineProviders = this.artifact.combineProviders || [];
this.providerNameList = [mainProviderName].concat(combineProviders);
}
get isReady() {
return this.initProgress === this.providerNameList.length;
}
getRenderContext(extendRenderContext = {}) {
const config = this.surgioConfig;
const gatewayConfig = config.gateway;
const gatewayToken = gatewayConfig?.viewerToken || gatewayConfig?.accessToken;
const { name: artifactName, downloadUrl } = this.artifact;
const { nodeList, netflixFilter, youtubePremiumFilter, customFilters } = this;
const remoteSnippets = lodash_1.default.keyBy(this.options.remoteSnippetList || [], (item) => item.name);
const mergedCustomParams = this.getMergedCustomParams(extendRenderContext);
return {
proxyTestUrl: config.proxyTestUrl,
proxyTestInterval: config.proxyTestInterval,
internetTestUrl: config.internetTestUrl,
internetTestInterval: config.internetTestInterval,
downloadUrl: downloadUrl
? downloadUrl
: (0, utils_1.getDownloadUrl)(config.urlBase, artifactName, true, gatewayToken),
snippet: (filePath) => {
return (0, template_1.loadLocalSnippet)(config.templateDir, filePath);
},
remoteSnippets,
nodeList,
provider: this.artifact.provider,
providerName: this.artifact.provider,
artifactName,
getDownloadUrl: (name) => (0, utils_1.getDownloadUrl)(config.urlBase, name, true, gatewayToken),
getUrl: (p) => (0, utils_1.getUrl)(config.publicUrl, p, gatewayToken),
getNodeNames: utils_1.getNodeNames,
getClashNodes: utils_1.getClashNodes,
getClashNodeNames: utils_1.getClashNodeNames,
getSingboxNodes: utils_1.getSingboxNodes,
getSingboxNodeNames: utils_1.getSingboxNodeNames,
getSurgeNodes: utils_1.getSurgeNodes,
getSurgeNodeNames: utils_1.getSurgeNodeNames,
getSurgeWireguardNodes: utils_1.getSurgeWireguardNodes,
getSurfboardNodes: utils_1.getSurfboardNodes,
getSurfboardNodeNames: utils_1.getSurfboardNodeNames,
getShadowsocksNodes: utils_1.getShadowsocksNodes,
getShadowsocksNodesJSON: utils_1.getShadowsocksNodesJSON,
getShadowsocksrNodes: utils_1.getShadowsocksrNodes,
getV2rayNNodes: utils_1.getV2rayNNodes,
getQuantumultXNodes: utils_1.getQuantumultXNodes,
getQuantumultXNodeNames: utils_1.getQuantumultXNodeNames,
getLoonNodes: utils_1.getLoonNodes,
getLoonNodeNames: utils_1.getLoonNodeNames,
toUrlSafeBase64: utils_1.toUrlSafeBase64,
toBase64: utils_1.toBase64,
encodeURIComponent,
...filters_1.internalFilters,
netflixFilter,
youtubePremiumFilter,
customFilters,
customParams: mergedCustomParams,
};
}
async init(params = {}) {
if (this.isReady) {
throw new Error('Artifact 已经初始化完成');
}
this.emit('initArtifact:start', { artifact: this.artifact });
await bluebird_1.default.map(this.providerNameList, async (providerName) => {
await this.providerMapper(providerName, params.getNodeListParams);
}, {
concurrency: (0, utils_1.getNetworkConcurrency)(),
});
this.providerNameList.forEach((providerName) => {
const nodeConfigList = this.nodeConfigListMap.get(providerName);
if (nodeConfigList) {
nodeConfigList.forEach((nodeConfig) => {
if (nodeConfig) {
this.nodeList.push(nodeConfig);
}
});
}
});
this.emit('initArtifact:end', { artifact: this.artifact });
return this;
}
getMergedCustomParams(extendableCustomParams = {}) {
const globalCustomParams = this.surgioConfig.customParams;
const { customParams: artifactCustomParams } = this.artifact;
const merged = lodash_1.default.merge({}, globalCustomParams, artifactCustomParams, extendableCustomParams);
return Object.freeze(merged);
}
render(templateEngine, extendRenderContext) {
if (!this.isReady) {
throw new Error('Artifact 还未初始化');
}
const targetTemplateEngine = templateEngine || this.options.templateEngine;
if (!targetTemplateEngine) {
throw new Error('没有可用的 Nunjucks 环境');
}
if (this.artifact.templateType === 'json' &&
!this.artifact.extendTemplate) {
throw new Error('JSON 模板需要提供 extendTemplate 函数');
}
const renderContext = this.getRenderContext(extendRenderContext);
const { templateString, template, templateType } = this.artifact;
const result = templateString
? targetTemplateEngine.renderString(templateString, {
templateEngine: targetTemplateEngine,
...renderContext,
})
: templateType === 'default'
? targetTemplateEngine.render(`${template}.tpl`, {
templateEngine: targetTemplateEngine,
...renderContext,
})
: (0, json_template_1.render)(this.surgioConfig.templateDir, `${template}.json`, this.artifact.extendTemplate, renderContext);
this.emit('renderArtifact', { artifact: this.artifact, result });
return result;
}
async providerMapper(providerName, getNodeListParams = {}) {
const config = this.surgioConfig;
const mainProviderName = this.artifact.provider;
const filePath = path_1.default.resolve(config.providerDir, `${providerName}.js`);
this.emit('initProvider:start', {
artifact: this.artifact,
providerName,
});
if (!fs_extra_1.default.existsSync(filePath)) {
throw new Error(`文件 ${filePath} 不存在`);
}
let provider;
let nodeConfigList;
try {
// eslint-disable-next-line prefer-const
provider = await (0, provider_1.getProvider)(providerName, require(filePath));
this.providerMap.set(providerName, provider);
}
catch (err) /* istanbul ignore next */ {
if ((0, utils_1.isSurgioError)(err)) {
err.providerName = providerName;
err.providerPath = filePath;
throw err;
}
else {
throw new utils_1.SurgioError((0, utils_1.isError)(err) ? err.message : '处理 Provider 失败', {
cause: err,
providerName,
providerPath: filePath,
});
}
}
try {
try {
nodeConfigList = await provider.getNodeList(this.getMergedCustomParams(getNodeListParams));
}
catch (err) {
if (provider.config.hooks?.onError && (0, utils_1.isError)(err)) {
const result = await provider.config.hooks.onError(err);
if (Array.isArray(result)) {
const adHocProvider = new provider_1.CustomProvider('ad-hoc', {
type: types_1.SupportProviderEnum.Custom,
nodeList: result,
});
nodeConfigList = await adHocProvider.getNodeList();
}
else {
nodeConfigList = [];
}
}
else {
throw err;
}
}
}
catch (err) /* istanbul ignore next */ {
if ((0, utils_1.isSurgioError)(err)) {
err.providerName = providerName;
err.providerPath = filePath;
throw err;
}
else {
throw new utils_1.SurgioError((0, utils_1.isError)(err) ? err.message : '处理 Provider 失败', {
cause: err,
providerName,
providerPath: filePath,
});
}
}
// Filter 仅使用第一个 Provider 中的定义
if (providerName === mainProviderName) {
if (provider.config.netflixFilter !== undefined) {
this.netflixFilter = provider.config.netflixFilter;
}
if (provider.config.youtubePremiumFilter !== undefined) {
this.youtubePremiumFilter = provider.config.youtubePremiumFilter;
}
this.customFilters = {
...this.customFilters,
...config.customFilters,
...provider.config.customFilters,
};
}
if ((0, filters_1.validateFilter)(provider.config.nodeFilter) &&
typeof provider.config.nodeFilter === 'object' &&
provider.config.nodeFilter.supportSort) {
nodeConfigList = provider.config.nodeFilter.filter(nodeConfigList);
}
nodeConfigList = (await bluebird_1.default.map(nodeConfigList, async (nodeConfig) => {
let isValid = false;
if (nodeConfig.enable === false) {
return undefined;
}
if (!provider.config.nodeFilter) {
isValid = true;
}
else if ((0, filters_1.validateFilter)(provider.config.nodeFilter)) {
isValid =
typeof provider.config.nodeFilter === 'function'
? provider.config.nodeFilter(nodeConfig)
: true;
}
if (isValid) {
if (config.binPath &&
nodeConfig.type === types_1.NodeTypeEnum.Shadowsocksr &&
config.binPath[nodeConfig.type]) {
nodeConfig.binPath = config.binPath[nodeConfig.type];
nodeConfig.localPort = provider.nextPort;
}
nodeConfig.provider = provider;
nodeConfig.surgeConfig = Object.freeze({
...config.surgeConfig,
...nodeConfig.surgeConfig,
});
nodeConfig.clashConfig = Object.freeze({
...config.clashConfig,
...nodeConfig.clashConfig,
});
nodeConfig.quantumultXConfig = Object.freeze({
...config.quantumultXConfig,
...nodeConfig.quantumultXConfig,
});
nodeConfig.surfboardConfig = Object.freeze({
...config.surfboardConfig,
...nodeConfig.surfboardConfig,
});
if (provider.config.renameNode) {
const newName = provider.config.renameNode(nodeConfig.nodeName);
if (newName) {
nodeConfig.nodeName = newName;
}
}
if (provider.config.addFlag) {
// 给节点名加国旗
nodeConfig.nodeName = (0, flag_1.prependFlag)(nodeConfig.nodeName, provider.config.removeExistingFlag);
}
else if (provider.config.removeExistingFlag) {
// 去掉名称中的国旗
nodeConfig.nodeName = (0, flag_1.removeFlag)(nodeConfig.nodeName);
}
// TCP Fast Open
if (typeof nodeConfig.tfo === 'undefined' && provider.config.tfo) {
nodeConfig.tfo = provider.config.tfo;
}
// MPTCP
if (typeof nodeConfig.mptcp === 'undefined' &&
provider.config.mptcp) {
nodeConfig.mptcp = provider.config.mptcp;
}
// ECN
if (typeof nodeConfig.ecn === 'undefined' && provider.config.ecn) {
nodeConfig.ecn = provider.config.ecn;
}
// Block QUIC
if (typeof nodeConfig.blockQuic === 'undefined' &&
provider.config.blockQuic) {
nodeConfig.blockQuic = provider.config.blockQuic;
}
// Underlying Proxy
if (!nodeConfig.underlyingProxy && provider.config.underlyingProxy) {
nodeConfig.underlyingProxy = provider.config.underlyingProxy;
}
// Check whether the hostname resolves in case of blocking clash's node heurestic
if (config?.checkHostname &&
'hostname' in nodeConfig &&
!(0, utils_1.isIp)(nodeConfig.hostname)) {
try {
const domains = await (0, dns_1.resolveDomain)(nodeConfig.hostname);
/* istanbul ignore next */
if (domains.length < 1) {
logger_1.logger.warn(`DNS 解析结果中 ${nodeConfig.hostname} 未有对应 IP 地址,将忽略该节点`);
return undefined;
} /* istanbul ignore next */
else {
nodeConfig.hostnameIp = domains;
}
}
catch (err) /* istanbul ignore next */ {
logger_1.logger.warn(`${nodeConfig.hostname} 无法解析,将忽略该节点`);
return undefined;
}
}
if (config?.resolveHostname &&
'hostname' in nodeConfig &&
!(0, utils_1.isIp)(nodeConfig.hostname)) {
/* istanbul ignore next */
if (nodeConfig.hostnameIp) {
nodeConfig.hostname = nodeConfig.hostnameIp[0];
} /* istanbul ignore next */
else {
try {
nodeConfig.hostnameIp = await (0, dns_1.resolveDomain)(nodeConfig.hostname);
nodeConfig.hostname = nodeConfig.hostnameIp[0];
}
catch (err) {
logger_1.logger.warn(`${nodeConfig.hostname} 无法解析,将忽略该域名的解析结果`);
}
}
}
return nodeConfig;
}
return undefined;
})).filter((item) => item !== undefined);
this.nodeConfigListMap.set(providerName, nodeConfigList);
this.initProgress++;
this.emit('initProvider:end', {
artifact: this.artifact,
providerName,
provider,
});
}
}
exports.Artifact = Artifact;
//# sourceMappingURL=artifact.js.map