surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
185 lines • 7.53 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseJSONConfig = exports.getV2rayNSubscription = void 0;
const assert_1 = __importDefault(require("assert"));
const logger_1 = require("@surgio/logger");
const zod_1 = require("zod");
const lodash_1 = __importDefault(require("lodash"));
const types_1 = require("../types");
const utils_1 = require("../utils");
const relayable_url_1 = __importDefault(require("../utils/relayable-url"));
const ss_1 = require("../utils/ss");
const Provider_1 = __importDefault(require("./Provider"));
class V2rayNSubscribeProvider extends Provider_1.default {
compatibleMode;
skipCertVerify;
udpRelay;
tls13;
#originalUrl;
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(),
compatibleMode: zod_1.z.boolean().optional(),
skipCertVerify: zod_1.z.boolean().optional(),
});
const result = schema.safeParse(config);
// istanbul ignore next
if (!result.success) {
throw new utils_1.SurgioError('V2rayNSubscribeProvider 配置校验失败', {
cause: result.error,
providerName: name,
});
}
this.#originalUrl = result.data.url;
this.compatibleMode = result.data.compatibleMode;
this.skipCertVerify = result.data.skipCertVerify;
this.tls13 = result.data.tls13;
this.udpRelay = result.data.udpRelay;
}
// istanbul ignore next
get url() {
return (0, relayable_url_1.default)(this.#originalUrl, this.config.relayUrl);
}
getNodeList = async (params = {}) => {
const requestHeaders = this.determineRequestHeaders(params.requestUserAgent, params.requestHeaders);
const cacheKey = Provider_1.default.getResourceCacheKey(requestHeaders, this.url);
const nodeList = await (0, exports.getV2rayNSubscription)({
url: this.url,
skipCertVerify: this.skipCertVerify,
tls13: this.tls13,
udpRelay: this.udpRelay,
isCompatibleMode: this.compatibleMode,
requestHeaders,
cacheKey,
});
if (this.config.hooks?.afterNodeListResponse) {
const newList = await this.config.hooks.afterNodeListResponse(nodeList, params);
if (newList) {
return newList;
}
}
return nodeList;
};
getNodeListV2 = async (params = {}) => {
const nodeList = await this.getNodeList(params);
return { nodeList };
};
}
exports.default = V2rayNSubscribeProvider;
/**
* @see 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 getV2rayNSubscription = async ({ url, isCompatibleMode, skipCertVerify, tls13, udpRelay, requestHeaders, cacheKey, }) => {
(0, assert_1.default)(url, '未指定订阅地址 url');
if (isCompatibleMode) {
logger_1.logger.warn('运行在兼容模式,请注意生成的节点是否正确。');
}
async function requestConfigFromRemote() {
const response = await Provider_1.default.requestCacheableResource(url, requestHeaders, cacheKey);
const configString = response.body;
const configList = (0, utils_1.fromBase64)(configString)
.split('\n')
.filter((item) => !!item)
.filter((item) => {
const pick = item.startsWith('vmess://') || item.startsWith('ss://');
if (!pick) {
logger_1.logger.warn(`不支持读取 V2rayN 订阅中的节点 ${item},该节点会被省略。`);
}
return pick;
});
return configList
.map((item) => {
if (item.startsWith('vmess://')) {
return (0, exports.parseJSONConfig)((0, utils_1.fromBase64)(item.replace('vmess://', '')), isCompatibleMode, skipCertVerify, udpRelay, tls13);
}
if (item.startsWith('ss://')) {
return {
...(0, ss_1.parseSSUri)(item),
udpRelay: udpRelay === true,
skipCertVerify: skipCertVerify === true,
tls13: tls13 === true,
};
}
return undefined;
})
.filter((item) => !!item);
}
return await requestConfigFromRemote();
};
exports.getV2rayNSubscription = getV2rayNSubscription;
const parseJSONConfig = (json, isCompatibleMode, skipCertVerify, udpRelay, tls13) => {
const config = JSON.parse(json);
// istanbul ignore next
if (!isCompatibleMode && (!config.v || Number(config.v) !== 2)) {
throw new Error(`该节点 ${config.ps} 可能不是一个有效的 V2rayN 节点。请参考 https://url.royli.dev/Qtrci 进行排查,或者将解析模式改为兼容模式`);
}
// istanbul ignore next
if (!['tcp', 'ws', 'h2', 'grpc'].includes(config.net)) {
logger_1.logger.warn(`不支持读取 network 类型为 ${config.net} 的 Vmess 节点,节点 ${config.ps} 会被省略。`);
return undefined;
}
// istanbul ignore next
if (!['none', 'http'].includes(config.type)) {
logger_1.logger.warn(`不支持读取 type 类型为 ${config.type} 的 Vmess 节点,节点 ${config.ps} 会被省略。`);
return undefined;
}
const vmessNode = {
nodeName: config.ps,
type: types_1.NodeTypeEnum.Vmess,
hostname: config.add,
port: config.port,
method: config.scy || 'auto',
uuid: config.id,
alterId: config.aid || '0',
network: config.net,
udpRelay: udpRelay === true,
tls: config.tls === 'tls',
};
if (vmessNode.tls) {
if (skipCertVerify) {
vmessNode.skipCertVerify = true;
}
if (tls13) {
vmessNode.tls13 = true;
}
if (config.sni) {
vmessNode.sni = config.sni;
}
}
switch (config.net) {
case 'tcp':
if (config.type === 'http') {
vmessNode.network = 'http';
lodash_1.default.set(vmessNode, 'httpOpts.path', [config.path || '/']);
lodash_1.default.set(vmessNode, 'httpOpts.method', 'GET');
if (config.host) {
lodash_1.default.set(vmessNode, 'httpOpts.headers.Host', config.host);
}
}
break;
case 'ws':
lodash_1.default.set(vmessNode, 'wsOpts.path', config.path || '/');
if (config.host) {
lodash_1.default.set(vmessNode, 'wsOpts.headers.Host', config.host);
}
break;
case 'h2':
lodash_1.default.set(vmessNode, 'h2Opts.path', config.path || '/');
if (config.host) {
lodash_1.default.set(vmessNode, 'h2Opts.host', [config.host]);
}
break;
case 'grpc':
lodash_1.default.set(vmessNode, 'grpcOpts.serviceName', config.path);
break;
}
return vmessNode;
};
exports.parseJSONConfig = parseJSONConfig;
//# sourceMappingURL=V2rayNSubscribeProvider.js.map