|
@@ -4,6 +4,7 @@ const { SocksProxyAgent } = require('socks-proxy-agent');
|
|
|
const { HttpProxyAgent } = require('http-proxy-agent');
|
|
|
const logger = require('../utils/logger');
|
|
|
const { TestResult } = require('../models');
|
|
|
+const net = require('net');
|
|
|
|
|
|
class SpeedTester {
|
|
|
constructor() {
|
|
@@ -17,7 +18,37 @@ class SpeedTester {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 测试单个节点连通性
|
|
|
+ * TCP端口连通性检测
|
|
|
+ */
|
|
|
+ async testTcpConnectivity(server, port, timeout = 5000) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const socket = new net.Socket();
|
|
|
+ let isConnected = false;
|
|
|
+
|
|
|
+ socket.setTimeout(timeout);
|
|
|
+
|
|
|
+ socket.on('connect', () => {
|
|
|
+ isConnected = true;
|
|
|
+ socket.destroy();
|
|
|
+ resolve({ success: true });
|
|
|
+ });
|
|
|
+
|
|
|
+ socket.on('timeout', () => {
|
|
|
+ socket.destroy();
|
|
|
+ if (!isConnected) resolve({ success: false, error: '连接超时' });
|
|
|
+ });
|
|
|
+
|
|
|
+ socket.on('error', (err) => {
|
|
|
+ socket.destroy();
|
|
|
+ if (!isConnected) resolve({ success: false, error: err.message });
|
|
|
+ });
|
|
|
+
|
|
|
+ socket.connect(port, server);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 测试单个节点连通性(只做TCP端口探测)
|
|
|
*/
|
|
|
async testNode(node) {
|
|
|
const startTime = Date.now();
|
|
@@ -39,28 +70,24 @@ class SpeedTester {
|
|
|
try {
|
|
|
logger.info(`开始测试节点连通性: ${node.name} (${node.type})`);
|
|
|
|
|
|
- // 创建代理配置
|
|
|
- const proxyConfig = this.createProxyConfig(node);
|
|
|
- if (!proxyConfig) {
|
|
|
- throw new Error(`不支持的代理类型: ${node.type}`);
|
|
|
+ // 验证节点配置的有效性
|
|
|
+ const validationResult = this.validateNode(node);
|
|
|
+ if (!validationResult.isValid) {
|
|
|
+ throw new Error(validationResult.error);
|
|
|
}
|
|
|
|
|
|
- // 只测试基本连通性
|
|
|
- const connectivityResult = await this.testBasicConnectivity(node, proxyConfig);
|
|
|
- testResult.isSuccess = connectivityResult.success;
|
|
|
- testResult.latency = connectivityResult.latency;
|
|
|
- testResult.testUrl = connectivityResult.url;
|
|
|
- testResult.ipAddress = connectivityResult.ipAddress;
|
|
|
- testResult.location = connectivityResult.location;
|
|
|
+ // 直接检测 server:port 的 TCP 连通性
|
|
|
+ const tcpResult = await this.testTcpConnectivity(node.server, node.port, this.timeout);
|
|
|
+ testResult.isSuccess = tcpResult.success;
|
|
|
+ testResult.errorMessage = tcpResult.error || null;
|
|
|
|
|
|
testResult.testDuration = Date.now() - startTime;
|
|
|
-
|
|
|
+
|
|
|
if (testResult.isSuccess) {
|
|
|
- logger.info(`✅ 节点连通性测试成功: ${node.name} - 延迟: ${testResult.latency}ms - IP: ${testResult.ipAddress}`);
|
|
|
+ logger.info(`✅ 节点TCP连通性测试成功: ${node.name}`);
|
|
|
} else {
|
|
|
- logger.warn(`❌ 节点连通性测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
|
+ logger.warn(`❌ 节点TCP连通性测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
|
}
|
|
|
-
|
|
|
} catch (error) {
|
|
|
testResult.errorMessage = error.message;
|
|
|
testResult.testDuration = Date.now() - startTime;
|
|
@@ -73,6 +100,69 @@ class SpeedTester {
|
|
|
return testResult;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 验证节点配置的有效性
|
|
|
+ */
|
|
|
+ validateNode(node) {
|
|
|
+ // 检查必需的字段
|
|
|
+ if (!node.name) {
|
|
|
+ return { isValid: false, error: '节点名称不能为空' };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!node.type) {
|
|
|
+ return { isValid: false, error: '节点类型不能为空' };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!node.server) {
|
|
|
+ return { isValid: false, error: '服务器地址不能为空' };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!node.port || node.port <= 0 || node.port > 65535) {
|
|
|
+ return { isValid: false, error: '端口号无效' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证server字段是否为有效的域名或IP地址
|
|
|
+ const server = node.server.trim();
|
|
|
+
|
|
|
+ // 只检查明显无效的情况,而不是过于严格的验证
|
|
|
+ // 检查是否包含中文字符(通常表示这是描述而不是有效地址)
|
|
|
+ if (/[\u4e00-\u9fa5]/.test(server)) {
|
|
|
+ return { isValid: false, error: `服务器地址包含中文字符,无效: ${server}` };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是URL格式(不应该作为server地址)
|
|
|
+ if (server.startsWith('http://') || server.startsWith('https://')) {
|
|
|
+ return { isValid: false, error: `服务器地址不能是URL格式: ${server}` };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否包含明显的无效字符(如空格、特殊符号等)
|
|
|
+ if (/[<>:"\\|?*]/.test(server)) {
|
|
|
+ return { isValid: false, error: `服务器地址包含无效字符: ${server}` };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为空或只包含空白字符
|
|
|
+ if (!server || server.trim() === '') {
|
|
|
+ return { isValid: false, error: '服务器地址不能为空' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于SS/SSR节点,检查密码
|
|
|
+ if ((node.type === 'ss' || node.type === 'ssr') && !node.password) {
|
|
|
+ return { isValid: false, error: 'SS/SSR节点必须设置密码' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于Vmess节点,检查UUID
|
|
|
+ if (node.type === 'vmess' && !node.uuid) {
|
|
|
+ return { isValid: false, error: 'Vmess节点必须设置UUID' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于Trojan节点,检查密码
|
|
|
+ if (node.type === 'trojan' && !node.password) {
|
|
|
+ return { isValid: false, error: 'Trojan节点必须设置密码' };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { isValid: true, error: null };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 创建代理配置
|
|
|
*/
|
|
@@ -229,6 +319,9 @@ class SpeedTester {
|
|
|
return this.testDirectConnectivity();
|
|
|
}
|
|
|
|
|
|
+ // 对于高级代理类型,我们仍然尝试通过本地代理测试
|
|
|
+ // 如果本地代理不可用,会在makeRequest中失败,这是正常的
|
|
|
+
|
|
|
// 尝试多个测试URL
|
|
|
for (const url of this.testUrls) {
|
|
|
try {
|
|
@@ -267,6 +360,8 @@ class SpeedTester {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 测试直连连通性
|
|
|
*/
|
|
@@ -381,7 +476,7 @@ class SpeedTester {
|
|
|
logger.info(`通过本地Clash代理测试: ${proxyConfig.type} - ${node.name}`);
|
|
|
|
|
|
// 使用本地Clash混合端口
|
|
|
- const localProxyUrl = 'http://127.0.0.1:7897';
|
|
|
+ const localProxyUrl = 'http://127.0.0.1:7890';
|
|
|
const localAgent = new HttpsProxyAgent(localProxyUrl);
|
|
|
logger.debug(`使用本地Clash代理: ${node.name} - ${localProxyUrl}`);
|
|
|
return localAgent;
|
|
@@ -482,4 +577,4 @@ class SpeedTester {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-module.exports = SpeedTester;
|
|
|
+module.exports = SpeedTester;
|