speedTester.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. const axios = require('axios');
  2. const { HttpsProxyAgent } = require('https-proxy-agent');
  3. const { SocksProxyAgent } = require('socks-proxy-agent');
  4. const { HttpProxyAgent } = require('http-proxy-agent');
  5. const logger = require('../utils/logger');
  6. const { TestResult } = require('../models');
  7. const net = require('net');
  8. const { exec } = require('child_process');
  9. class SpeedTester {
  10. constructor() {
  11. this.testUrls = process.env.SPEED_TEST_URLS?.split(',') || [
  12. 'https://www.google.com',
  13. 'https://www.youtube.com',
  14. 'https://www.github.com',
  15. 'https://httpbin.org/get'
  16. ];
  17. this.timeout = parseInt(process.env.SPEED_TEST_TIMEOUT) || 10000;
  18. }
  19. /**
  20. * TCP端口连通性检测 - 使用tcp-ping库改进
  21. */
  22. async testTcpConnectivity(server, port, timeout = 5000) {
  23. return new Promise((resolve) => {
  24. try {
  25. // 尝试使用tcp-ping库,如果不可用则回退到原生实现
  26. const tcpPing = require('tcp-ping');
  27. tcpPing.ping({
  28. address: server,
  29. port: port,
  30. attempts: 3,
  31. timeout: timeout
  32. }, (err, data) => {
  33. if (err) {
  34. logger.warn(`tcp-ping库不可用,使用原生实现: ${err.message}`);
  35. this.testTcpConnectivityNative(server, port, timeout).then(resolve);
  36. return;
  37. }
  38. if (data && data.min !== undefined) {
  39. // tcp-ping库返回的时间单位是秒,需要转换为毫秒
  40. resolve({
  41. success: true,
  42. latency: Math.round(data.min * 1000),
  43. avg: Math.round(data.avg * 1000),
  44. max: Math.round(data.max * 1000),
  45. min: Math.round(data.min * 1000)
  46. });
  47. } else {
  48. resolve({ success: false, error: '连接失败', latency: -1 });
  49. }
  50. });
  51. } catch (error) {
  52. logger.warn(`tcp-ping库加载失败,使用原生实现: ${error.message}`);
  53. this.testTcpConnectivityNative(server, port, timeout).then(resolve);
  54. }
  55. });
  56. }
  57. /**
  58. * 原生TCP连通性检测(备用方案)
  59. */
  60. async testTcpConnectivityNative(server, port, timeout = 5000) {
  61. return new Promise((resolve) => {
  62. const socket = new net.Socket();
  63. let isConnected = false;
  64. const startTime = Date.now();
  65. socket.setTimeout(timeout);
  66. socket.on('connect', () => {
  67. isConnected = true;
  68. const latency = Date.now() - startTime;
  69. socket.destroy();
  70. resolve({
  71. success: true,
  72. latency: latency,
  73. avg: latency,
  74. max: latency,
  75. min: latency
  76. });
  77. });
  78. socket.on('timeout', () => {
  79. socket.destroy();
  80. if (!isConnected) resolve({
  81. success: false,
  82. error: '连接超时',
  83. latency: -1
  84. });
  85. });
  86. socket.on('error', (err) => {
  87. socket.destroy();
  88. if (!isConnected) resolve({
  89. success: false,
  90. error: err.message,
  91. latency: -1
  92. });
  93. });
  94. socket.connect(port, server);
  95. });
  96. }
  97. /**
  98. * 使用系统 ping 命令测真实网络延迟(ICMP RTT)
  99. * 支持 Windows 和 Linux
  100. */
  101. async systemPing(host, attempts = 3, timeout = 2000) {
  102. return new Promise((resolve) => {
  103. // 判断平台
  104. const isWin = process.platform === 'win32';
  105. // Windows: ping -n 次数 -w 超时 host
  106. // Linux/Mac: ping -c 次数 -W 超时 host
  107. let cmd;
  108. if (isWin) {
  109. // -n 次数, -w 单次超时(毫秒)
  110. cmd = `ping -n ${attempts} -w ${timeout} ${host}`;
  111. } else {
  112. // -c 次数, -W 单次超时(秒)
  113. cmd = `ping -c ${attempts} -W ${Math.ceil(timeout/1000)} ${host}`;
  114. }
  115. exec(cmd, (error, stdout) => {
  116. if (error) {
  117. resolve({ success: false, error: error.message, raw: stdout });
  118. return;
  119. }
  120. let avg, min, max, results = [];
  121. if (isWin) {
  122. // 解析每次延迟
  123. const msMatches = [...stdout.matchAll(/时间[=<]([\d]+)ms/g)];
  124. results = msMatches.map((m, i) => ({ seq: i+1, time: parseInt(m[1], 10) }));
  125. // 解析统计
  126. const statMatch = stdout.match(/平均 = (\d+)ms/);
  127. avg = statMatch ? parseInt(statMatch[1], 10) : null;
  128. const minMatch = stdout.match(/最短 = (\d+)ms/);
  129. min = minMatch ? parseInt(minMatch[1], 10) : null;
  130. const maxMatch = stdout.match(/最长 = (\d+)ms/);
  131. max = maxMatch ? parseInt(maxMatch[1], 10) : null;
  132. } else {
  133. // 解析每次延迟
  134. const msMatches = [...stdout.matchAll(/time=([\d.]+) ms/g)];
  135. results = msMatches.map((m, i) => ({ seq: i+1, time: Math.round(parseFloat(m[1])) }));
  136. // 解析统计
  137. const statMatch = stdout.match(/min\/avg\/max\/.* = ([\d.]+)\/([\d.]+)\/([\d.]+)\//);
  138. min = statMatch ? Math.round(parseFloat(statMatch[1])) : null;
  139. avg = statMatch ? Math.round(parseFloat(statMatch[2])) : null;
  140. max = statMatch ? Math.round(parseFloat(statMatch[3])) : null;
  141. }
  142. if (avg != null) {
  143. resolve({ success: true, host, avg, min, max, attempts, results, raw: stdout });
  144. } else {
  145. resolve({ success: false, error: '无法解析ping输出', raw: stdout });
  146. }
  147. });
  148. });
  149. }
  150. /**
  151. * 核心ping测速实现 - 参考Electron应用的方式
  152. * 使用tcp-ping库进行精确的延迟测量
  153. */
  154. async pingHosts(host, port, attempts = 3, timeout = 2000) {
  155. return new Promise((resolve) => {
  156. try {
  157. const tcpPing = require('tcp-ping');
  158. tcpPing.ping({
  159. address: host,
  160. port: port,
  161. attempts: attempts,
  162. timeout: timeout
  163. }, (err, data) => {
  164. if (err) {
  165. logger.warn(`tcp-ping测速失败: ${host}:${port} - ${err.message}`);
  166. resolve({ success: false, latency: -1, error: err.message });
  167. return;
  168. }
  169. if (data && data.min !== undefined) {
  170. // tcp-ping返回的时间单位是秒,转换为毫秒
  171. const result = {
  172. success: true,
  173. host: host,
  174. port: port,
  175. attempts: data.attempts,
  176. avg: Math.round(data.avg * 1000),
  177. max: Math.round(data.max * 1000),
  178. min: Math.round(data.min * 1000),
  179. latency: Math.round(data.min * 1000), // 使用最小延迟作为主要延迟值
  180. results: data.results.map(r => ({
  181. seq: r.seq,
  182. time: Math.round(r.time * 1000)
  183. }))
  184. };
  185. logger.debug(`ping测速成功: ${host}:${port} - 延迟: ${result.latency}ms (平均: ${result.avg}ms, 最大: ${result.max}ms)`);
  186. resolve(result);
  187. } else {
  188. logger.warn(`ping测速失败: ${host}:${port} - 连接失败`);
  189. resolve({ success: false, latency: -1, error: '连接失败' });
  190. }
  191. });
  192. } catch (error) {
  193. logger.warn(`tcp-ping库不可用,使用原生实现: ${error.message}`);
  194. this.pingHostsNative(host, port, attempts, timeout).then(resolve);
  195. }
  196. });
  197. }
  198. /**
  199. * 原生TCP ping实现(备用方案)
  200. */
  201. async pingHostsNative(host, port, attempts = 3, timeout = 2000) {
  202. const results = [];
  203. for (let i = 0; i < attempts; i++) {
  204. try {
  205. const result = await this.singlePing(host, port, timeout);
  206. if (result.success) {
  207. results.push(result.latency);
  208. }
  209. } catch (error) {
  210. logger.debug(`单次ping失败: ${host}:${port} - 第${i+1}次尝试`);
  211. }
  212. }
  213. if (results.length > 0) {
  214. const min = Math.min(...results);
  215. const max = Math.max(...results);
  216. const avg = results.reduce((a, b) => a + b, 0) / results.length;
  217. return {
  218. success: true,
  219. host: host,
  220. port: port,
  221. attempts: attempts,
  222. avg: Math.round(avg),
  223. max: max,
  224. min: min,
  225. latency: min, // 使用最小延迟
  226. results: results.map((latency, index) => ({ seq: index + 1, time: latency }))
  227. };
  228. } else {
  229. return {
  230. success: false,
  231. host: host,
  232. port: port,
  233. latency: -1,
  234. error: '所有尝试都失败'
  235. };
  236. }
  237. }
  238. /**
  239. * 单次ping测试
  240. */
  241. async singlePing(host, port, timeout) {
  242. return new Promise((resolve) => {
  243. const socket = new net.Socket();
  244. let isConnected = false;
  245. const startTime = Date.now();
  246. socket.setTimeout(timeout);
  247. socket.on('connect', () => {
  248. isConnected = true;
  249. const latency = Date.now() - startTime;
  250. socket.destroy();
  251. resolve({ success: true, latency: latency });
  252. });
  253. socket.on('timeout', () => {
  254. socket.destroy();
  255. if (!isConnected) resolve({ success: false, error: '连接超时' });
  256. });
  257. socket.on('error', (err) => {
  258. socket.destroy();
  259. if (!isConnected) resolve({ success: false, error: err.message });
  260. });
  261. socket.connect(port, host);
  262. });
  263. }
  264. /**
  265. * 批量ping测速 - 改进版
  266. * 提供更精细的并发控制和更好的错误处理
  267. */
  268. async batchPingHosts(hosts, options = {}) {
  269. const {
  270. concurrency = 3, // 降低默认并发数
  271. batchDelay = 2000, // 增加批次间延迟
  272. retryAttempts = 1, // 失败重试次数
  273. timeout = 3000, // 增加超时时间
  274. attempts = 3 // 每次ping的尝试次数
  275. } = options;
  276. logger.info(`开始批量ping测速: ${hosts.length}个节点,并发数: ${concurrency}`);
  277. const results = [];
  278. const startTime = Date.now();
  279. // 分批处理,控制并发数
  280. for (let i = 0; i < hosts.length; i += concurrency) {
  281. const batch = hosts.slice(i, i + concurrency);
  282. logger.debug(`处理批次 ${Math.floor(i/concurrency) + 1}/${Math.ceil(hosts.length/concurrency)}: ${batch.length}个节点`);
  283. const batchPromises = batch.map(async (hostConfig, batchIndex) => {
  284. const { host, port } = hostConfig;
  285. const nodeName = `${host}:${port}`;
  286. // 添加随机延迟,避免同时发起请求
  287. const randomDelay = Math.random() * 500;
  288. await new Promise(resolve => setTimeout(resolve, randomDelay));
  289. let lastError = null;
  290. // 重试机制
  291. for (let retry = 0; retry <= retryAttempts; retry++) {
  292. try {
  293. const result = await this.pingHosts(host, port, attempts, timeout);
  294. return {
  295. host,
  296. port,
  297. ...result,
  298. retryCount: retry,
  299. batchIndex: batchIndex
  300. };
  301. } catch (error) {
  302. lastError = error;
  303. logger.debug(`ping测速失败 (重试 ${retry + 1}/${retryAttempts + 1}): ${nodeName} - ${error.message}`);
  304. if (retry < retryAttempts) {
  305. // 重试前等待
  306. await new Promise(resolve => setTimeout(resolve, 1000 * (retry + 1)));
  307. }
  308. }
  309. }
  310. // 所有重试都失败
  311. logger.warn(`ping测速最终失败: ${nodeName} - ${lastError?.message || '未知错误'}`);
  312. return {
  313. host,
  314. port,
  315. success: false,
  316. error: lastError?.message || '所有重试都失败',
  317. latency: -1,
  318. retryCount: retryAttempts,
  319. batchIndex: batchIndex
  320. };
  321. });
  322. const batchResults = await Promise.allSettled(batchPromises);
  323. batchResults.forEach((result, index) => {
  324. if (result.status === 'fulfilled') {
  325. results.push(result.value);
  326. } else {
  327. logger.error(`ping测速异常: ${batch[index].host}:${batch[index].port} - ${result.reason}`);
  328. results.push({
  329. host: batch[index].host,
  330. port: batch[index].port,
  331. success: false,
  332. error: result.reason,
  333. latency: -1,
  334. retryCount: 0,
  335. batchIndex: index
  336. });
  337. }
  338. });
  339. // 批次间延迟,避免过于频繁的请求
  340. if (i + concurrency < hosts.length) {
  341. logger.debug(`批次间延迟: ${batchDelay}ms`);
  342. await new Promise(resolve => setTimeout(resolve, batchDelay));
  343. }
  344. }
  345. // 按延迟排序
  346. const sortedResults = results.sort((a, b) => {
  347. if (a.success && b.success) {
  348. return (a.latency || a.min || 9999) - (b.latency || b.min || 9999);
  349. }
  350. return a.success ? -1 : 1;
  351. });
  352. const successCount = sortedResults.filter(r => r.success).length;
  353. const totalTime = Date.now() - startTime;
  354. logger.info(`批量ping测速完成: ${successCount}/${hosts.length}个节点连通,总耗时: ${totalTime}ms`);
  355. return {
  356. results: sortedResults,
  357. summary: {
  358. total: results.length,
  359. successful: successCount,
  360. failed: results.length - successCount,
  361. avgLatency: sortedResults
  362. .filter(r => r.success && r.latency)
  363. .reduce((sum, r) => sum + r.latency, 0) / sortedResults.filter(r => r.success && r.latency).length || 0,
  364. totalTime: totalTime,
  365. concurrency: concurrency,
  366. batchDelay: batchDelay
  367. }
  368. };
  369. }
  370. /**
  371. * 单次测速 - 优化版
  372. * 提供更精确的单节点测速
  373. */
  374. async singlePingOptimized(host, port, options = {}) {
  375. const {
  376. attempts = 5, // 增加尝试次数
  377. timeout = 5000, // 增加超时时间
  378. interval = 500 // 尝试间隔
  379. } = options;
  380. logger.info(`开始单次优化测速: ${host}:${port}`);
  381. const results = [];
  382. const startTime = Date.now();
  383. for (let i = 0; i < attempts; i++) {
  384. try {
  385. const result = await this.singlePing(host, port, timeout);
  386. if (result.success) {
  387. results.push(result.latency);
  388. logger.debug(`第${i + 1}次尝试成功: ${result.latency}ms`);
  389. } else {
  390. logger.debug(`第${i + 1}次尝试失败: ${result.error}`);
  391. }
  392. } catch (error) {
  393. logger.debug(`第${i + 1}次尝试异常: ${error.message}`);
  394. }
  395. // 尝试间隔
  396. if (i < attempts - 1) {
  397. await new Promise(resolve => setTimeout(resolve, interval));
  398. }
  399. }
  400. if (results.length > 0) {
  401. const min = Math.min(...results);
  402. const max = Math.max(...results);
  403. const avg = results.reduce((a, b) => a + b, 0) / results.length;
  404. const totalTime = Date.now() - startTime;
  405. logger.info(`单次优化测速完成: ${host}:${port} - 延迟: ${min}ms (平均: ${Math.round(avg)}ms, 最大: ${max}ms)`);
  406. return {
  407. success: true,
  408. host: host,
  409. port: port,
  410. attempts: attempts,
  411. avg: Math.round(avg),
  412. max: max,
  413. min: min,
  414. latency: min,
  415. results: results.map((latency, index) => ({ seq: index + 1, time: latency })),
  416. totalTime: totalTime
  417. };
  418. } else {
  419. const totalTime = Date.now() - startTime;
  420. logger.warn(`单次优化测速失败: ${host}:${port} - 所有尝试都失败`);
  421. return {
  422. success: false,
  423. host: host,
  424. port: port,
  425. attempts: attempts,
  426. latency: -1,
  427. error: '所有尝试都失败',
  428. totalTime: totalTime
  429. };
  430. }
  431. }
  432. /**
  433. * 测试单个节点连通性(改进版)
  434. */
  435. async testNode(node) {
  436. const startTime = Date.now();
  437. let testResult = {
  438. nodeId: node.id,
  439. testTime: new Date(),
  440. isSuccess: false,
  441. latency: null,
  442. downloadSpeed: null, // 保留字段但不测试
  443. uploadSpeed: null, // 保留字段但不测试
  444. packetLoss: null,
  445. testUrl: null,
  446. errorMessage: null,
  447. testDuration: null,
  448. ipAddress: null,
  449. location: null,
  450. pingStats: null // 新增ping统计信息
  451. };
  452. try {
  453. logger.info(`开始测试节点连通性: ${node.name} (${node.type})`);
  454. // 验证节点配置的有效性
  455. const validationResult = this.validateNode(node);
  456. if (!validationResult.isValid) {
  457. throw new Error(validationResult.error);
  458. }
  459. // 使用高级ping测试
  460. const pingResult = await this.pingHosts(node.server, node.port, 3, 2000);
  461. testResult.isSuccess = pingResult.success;
  462. testResult.latency = pingResult.min || pingResult.latency;
  463. testResult.errorMessage = pingResult.error || null;
  464. testResult.pingStats = pingResult.success ? {
  465. attempts: pingResult.attempts,
  466. avg: pingResult.avg,
  467. max: pingResult.max,
  468. min: pingResult.min,
  469. results: pingResult.results
  470. } : null;
  471. testResult.testDuration = Date.now() - startTime;
  472. if (testResult.isSuccess) {
  473. logger.info(`✅ 节点Ping测试成功: ${node.name} - 延迟: ${testResult.latency}ms (平均: ${pingResult.avg}ms, 最大: ${pingResult.max}ms)`);
  474. } else {
  475. logger.warn(`❌ 节点Ping测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
  476. }
  477. } catch (error) {
  478. testResult.errorMessage = error.message;
  479. testResult.testDuration = Date.now() - startTime;
  480. logger.error(`❌ 节点测试异常: ${node.name} - ${error.message}`);
  481. }
  482. // 保存测试结果
  483. await this.saveTestResult(testResult);
  484. return testResult;
  485. }
  486. /**
  487. * 验证节点配置的有效性
  488. */
  489. validateNode(node) {
  490. // 检查必需的字段
  491. if (!node.name) {
  492. return { isValid: false, error: '节点名称不能为空' };
  493. }
  494. if (!node.type) {
  495. return { isValid: false, error: '节点类型不能为空' };
  496. }
  497. if (!node.server) {
  498. return { isValid: false, error: '服务器地址不能为空' };
  499. }
  500. if (!node.port || node.port <= 0 || node.port > 65535) {
  501. return { isValid: false, error: '端口号无效' };
  502. }
  503. // 验证server字段是否为有效的域名或IP地址
  504. const server = node.server.trim();
  505. // 检查是否为空或只包含空白字符
  506. if (!server || server.trim() === '') {
  507. return { isValid: false, error: '服务器地址不能为空' };
  508. }
  509. // 检查是否是URL格式(不应该作为server地址)
  510. if (server.startsWith('http://') || server.startsWith('https://')) {
  511. return { isValid: false, error: `服务器地址不能是URL格式: ${server}` };
  512. }
  513. // 检查是否包含明显的无效字符(如空格、特殊符号等)
  514. if (/[<>:"\\|?*]/.test(server)) {
  515. return { isValid: false, error: `服务器地址包含无效字符: ${server}` };
  516. }
  517. // 允许包含中文的服务器地址
  518. if (/[\u4e00-\u9fa5]/.test(server)) {
  519. // 包含中文的地址直接通过验证
  520. logger.debug(`检测到包含中文的服务器地址: ${server}`);
  521. } else {
  522. // 对于不包含中文的域名,进行基本的域名格式验证
  523. if (!this.isValidBasicDomain(server)) {
  524. return { isValid: false, error: `服务器地址格式无效: ${server}` };
  525. }
  526. }
  527. // 对于SS/SSR节点,检查密码
  528. if ((node.type === 'ss' || node.type === 'ssr') && !node.password) {
  529. return { isValid: false, error: 'SS/SSR节点必须设置密码' };
  530. }
  531. // 对于Vmess节点,检查UUID
  532. if (node.type === 'vmess' && !node.uuid) {
  533. return { isValid: false, error: 'Vmess节点必须设置UUID' };
  534. }
  535. // 对于Trojan节点,检查密码
  536. if (node.type === 'trojan' && !node.password) {
  537. return { isValid: false, error: 'Trojan节点必须设置密码' };
  538. }
  539. return { isValid: true, error: null };
  540. }
  541. /**
  542. * 验证是否为合法的中文域名
  543. * 支持以下格式:
  544. * - 纯中文域名:中文.域名.com
  545. * - 混合域名:中文.example.com
  546. * - 包含中文的域名:test.中文.com
  547. */
  548. isValidChineseDomain(domain) {
  549. // 移除前后空格
  550. domain = domain.trim();
  551. // 检查是否包含中文字符
  552. if (!/[\u4e00-\u9fa5]/.test(domain)) {
  553. return false;
  554. }
  555. // 检查是否包含域名分隔符(点号)
  556. if (!domain.includes('.')) {
  557. return false;
  558. }
  559. // 检查是否以点号开头或结尾
  560. if (domain.startsWith('.') || domain.endsWith('.')) {
  561. return false;
  562. }
  563. // 检查是否包含连续的点号
  564. if (domain.includes('..')) {
  565. return false;
  566. }
  567. // 检查每个标签的长度(域名标签不能超过63个字符)
  568. const labels = domain.split('.');
  569. for (const label of labels) {
  570. if (label.length === 0 || label.length > 63) {
  571. return false;
  572. }
  573. }
  574. // 检查顶级域名(最后一部分)是否至少包含一个非中文字符
  575. const tld = labels[labels.length - 1];
  576. if (!/[a-zA-Z0-9]/.test(tld)) {
  577. return false;
  578. }
  579. // 检查是否包含有效的域名字符(中文、英文、数字、连字符)
  580. const validDomainRegex = /^[a-zA-Z0-9\u4e00-\u9fa5\-\.]+$/;
  581. if (!validDomainRegex.test(domain)) {
  582. return false;
  583. }
  584. return true;
  585. }
  586. /**
  587. * 验证是否为合法的基本域名格式(不包含中文)
  588. */
  589. isValidBasicDomain(domain) {
  590. // 移除前后空格
  591. domain = domain.trim();
  592. // 检查是否包含域名分隔符(点号)
  593. if (!domain.includes('.')) {
  594. return false;
  595. }
  596. // 检查是否以点号开头或结尾
  597. if (domain.startsWith('.') || domain.endsWith('.')) {
  598. return false;
  599. }
  600. // 检查是否包含连续的点号
  601. if (domain.includes('..')) {
  602. return false;
  603. }
  604. // 检查每个标签的长度(域名标签不能超过63个字符)
  605. const labels = domain.split('.');
  606. for (const label of labels) {
  607. if (label.length === 0 || label.length > 63) {
  608. return false;
  609. }
  610. }
  611. // 检查是否包含有效的域名字符(英文、数字、连字符)
  612. const validDomainRegex = /^[a-zA-Z0-9\-\.]+$/;
  613. if (!validDomainRegex.test(domain)) {
  614. return false;
  615. }
  616. return true;
  617. }
  618. /**
  619. * 创建代理配置
  620. */
  621. createProxyConfig(node) {
  622. const baseConfig = {
  623. host: node.server,
  624. port: node.port,
  625. timeout: this.timeout
  626. };
  627. switch (node.type) {
  628. case 'ss':
  629. return this.createShadowsocksConfig(node, baseConfig);
  630. case 'ssr':
  631. return this.createShadowsocksRConfig(node, baseConfig);
  632. case 'vmess':
  633. return this.createVmessConfig(node, baseConfig);
  634. case 'trojan':
  635. return this.createTrojanConfig(node, baseConfig);
  636. case 'http':
  637. case 'https':
  638. return this.createHttpConfig(node, baseConfig);
  639. case 'socks5':
  640. return this.createSocks5Config(node, baseConfig);
  641. default:
  642. logger.warn(`不支持的代理类型: ${node.type} - ${node.name}`);
  643. return null;
  644. }
  645. }
  646. /**
  647. * 创建Shadowsocks代理配置
  648. */
  649. createShadowsocksConfig(node, baseConfig) {
  650. return {
  651. ...baseConfig,
  652. type: 'ss',
  653. password: node.password,
  654. method: node.cipher || node.method,
  655. auth: node.username && node.password ? {
  656. username: node.username,
  657. password: node.password
  658. } : null
  659. };
  660. }
  661. /**
  662. * 创建ShadowsocksR代理配置
  663. */
  664. createShadowsocksRConfig(node, baseConfig) {
  665. return {
  666. ...baseConfig,
  667. type: 'ssr',
  668. password: node.password,
  669. method: node.cipher || node.method,
  670. protocol: node.protocol,
  671. obfs: node.obfs,
  672. auth: node.username && node.password ? {
  673. username: node.username,
  674. password: node.password
  675. } : null
  676. };
  677. }
  678. /**
  679. * 创建Vmess代理配置
  680. */
  681. createVmessConfig(node, baseConfig) {
  682. return {
  683. ...baseConfig,
  684. type: 'vmess',
  685. uuid: node.uuid,
  686. alterId: node.alterId,
  687. network: node.network,
  688. tls: node.tls,
  689. sni: node.sni,
  690. wsPath: node.wsPath,
  691. wsHeaders: node.wsHeaders ? JSON.parse(node.wsHeaders) : {},
  692. auth: node.username && node.password ? {
  693. username: node.username,
  694. password: node.password
  695. } : null
  696. };
  697. }
  698. /**
  699. * 创建Trojan代理配置
  700. */
  701. createTrojanConfig(node, baseConfig) {
  702. return {
  703. ...baseConfig,
  704. type: 'trojan',
  705. password: node.password,
  706. tls: node.tls,
  707. sni: node.sni,
  708. auth: node.username && node.password ? {
  709. username: node.username,
  710. password: node.password
  711. } : null
  712. };
  713. }
  714. /**
  715. * 创建HTTP代理配置
  716. */
  717. createHttpConfig(node, baseConfig) {
  718. const config = {
  719. ...baseConfig,
  720. type: 'http'
  721. };
  722. if (node.username && node.password) {
  723. config.auth = {
  724. username: node.username,
  725. password: node.password
  726. };
  727. }
  728. if (node.tls) {
  729. config.https = true;
  730. config.sni = node.sni;
  731. }
  732. return config;
  733. }
  734. /**
  735. * 创建SOCKS5代理配置
  736. */
  737. createSocks5Config(node, baseConfig) {
  738. const config = {
  739. ...baseConfig,
  740. type: 'socks5'
  741. };
  742. if (node.username && node.password) {
  743. config.auth = {
  744. username: node.username,
  745. password: node.password
  746. };
  747. }
  748. return config;
  749. }
  750. /**
  751. * 测试基本连通性
  752. */
  753. async testBasicConnectivity(node, proxyConfig) {
  754. const startTime = Date.now();
  755. // 如果是直连测试
  756. if (node.type === 'direct') {
  757. return this.testDirectConnectivity();
  758. }
  759. // 对于高级代理类型,我们仍然尝试通过本地代理测试
  760. // 如果本地代理不可用,会在makeRequest中失败,这是正常的
  761. // 尝试多个测试URL
  762. for (const url of this.testUrls) {
  763. try {
  764. logger.debug(`尝试连接: ${node.name} -> ${url}`);
  765. const response = await this.makeRequest(node, proxyConfig, {
  766. method: 'HEAD',
  767. url: url,
  768. timeout: this.timeout,
  769. maxRedirects: 3
  770. });
  771. const latency = Date.now() - startTime;
  772. logger.debug(`连接成功: ${node.name} -> ${url} (${latency}ms)`);
  773. return {
  774. success: true,
  775. latency: latency,
  776. url: url,
  777. ipAddress: this.extractIpAddress(response),
  778. location: this.extractLocation(response)
  779. };
  780. } catch (error) {
  781. logger.debug(`连接失败: ${node.name} -> ${url} - ${error.message}`);
  782. continue;
  783. }
  784. }
  785. return {
  786. success: false,
  787. latency: null,
  788. url: null,
  789. ipAddress: null,
  790. location: null
  791. };
  792. }
  793. /**
  794. * 测试直连连通性
  795. */
  796. async testDirectConnectivity() {
  797. const startTime = Date.now();
  798. for (const url of this.testUrls) {
  799. try {
  800. logger.debug(`尝试直连: ${url}`);
  801. const response = await axios.get(url, {
  802. timeout: this.timeout,
  803. headers: {
  804. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
  805. }
  806. });
  807. const latency = Date.now() - startTime;
  808. logger.debug(`直连成功: ${url} (${latency}ms)`);
  809. return {
  810. success: true,
  811. latency: latency,
  812. url: url,
  813. ipAddress: this.extractIpAddress(response),
  814. location: this.extractLocation(response)
  815. };
  816. } catch (error) {
  817. logger.debug(`直连失败: ${url} - ${error.message}`);
  818. continue;
  819. }
  820. }
  821. return {
  822. success: false,
  823. latency: null,
  824. url: null,
  825. ipAddress: null,
  826. location: null
  827. };
  828. }
  829. /**
  830. * 发送HTTP请求
  831. */
  832. async makeRequest(node, proxyConfig, requestConfig) {
  833. const agent = this.createProxyAgent(node, proxyConfig);
  834. if (!agent) {
  835. throw new Error(`无法创建代理Agent: ${proxyConfig.type}`);
  836. }
  837. logger.debug(`使用代理: ${node.name} - ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`);
  838. const config = {
  839. ...requestConfig,
  840. httpsAgent: agent,
  841. httpAgent: agent,
  842. headers: {
  843. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  844. ...requestConfig.headers
  845. }
  846. };
  847. return axios(config);
  848. }
  849. /**
  850. * 创建代理Agent
  851. */
  852. createProxyAgent(node, proxyConfig) {
  853. try {
  854. logger.debug(`创建代理Agent: ${node.name} - ${proxyConfig.type}`);
  855. switch (proxyConfig.type) {
  856. case 'http':
  857. case 'https':
  858. // HTTP/HTTPS代理
  859. let httpProxyUrl = `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`;
  860. // 如果有认证信息,添加到URL中
  861. if (proxyConfig.auth) {
  862. const { username, password } = proxyConfig.auth;
  863. httpProxyUrl = `${proxyConfig.type}://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
  864. }
  865. const httpAgent = new HttpsProxyAgent(httpProxyUrl);
  866. logger.debug(`HTTP代理创建成功: ${node.name} - ${httpProxyUrl}`);
  867. return httpAgent;
  868. case 'socks5':
  869. // SOCKS5代理
  870. let socksProxyUrl = `socks5://${proxyConfig.host}:${proxyConfig.port}`;
  871. // 如果有认证信息,添加到URL中
  872. if (proxyConfig.auth) {
  873. const { username, password } = proxyConfig.auth;
  874. socksProxyUrl = `socks5://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
  875. }
  876. const socksAgent = new SocksProxyAgent(socksProxyUrl);
  877. logger.debug(`SOCKS5代理创建成功: ${node.name} - ${socksProxyUrl}`);
  878. return socksAgent;
  879. case 'ss':
  880. case 'ssr':
  881. case 'vmess':
  882. case 'trojan':
  883. // 对于这些高级代理类型,我们需要通过本地Clash代理来测试
  884. // 因为Node.js没有直接的vmess/ss客户端库
  885. logger.info(`通过本地Clash代理测试: ${proxyConfig.type} - ${node.name}`);
  886. // 使用本地Clash混合端口
  887. const localProxyUrl = 'http://127.0.0.1:7890';
  888. const localAgent = new HttpsProxyAgent(localProxyUrl);
  889. logger.debug(`使用本地Clash代理: ${node.name} - ${localProxyUrl}`);
  890. return localAgent;
  891. default:
  892. logger.warn(`不支持的代理类型: ${proxyConfig.type} - ${node.name}`);
  893. return null;
  894. }
  895. } catch (error) {
  896. logger.error(`创建代理Agent失败: ${node.name}`, { error: error.message, type: proxyConfig.type });
  897. return null;
  898. }
  899. }
  900. /**
  901. * 提取IP地址
  902. */
  903. extractIpAddress(response) {
  904. // 从响应头中提取IP地址
  905. return response.headers['x-forwarded-for'] ||
  906. response.headers['x-real-ip'] ||
  907. response.headers['cf-connecting-ip'] ||
  908. 'unknown';
  909. }
  910. /**
  911. * 提取位置信息
  912. */
  913. extractLocation(response) {
  914. // 从响应头中提取位置信息
  915. if (response.headers['cf-ray']) {
  916. return 'Cloudflare';
  917. }
  918. if (response.headers['cf-ipcountry']) {
  919. return response.headers['cf-ipcountry'];
  920. }
  921. return 'unknown';
  922. }
  923. /**
  924. * 保存测试结果
  925. */
  926. async saveTestResult(testResult) {
  927. try {
  928. await TestResult.create(testResult);
  929. } catch (error) {
  930. logger.error(`保存测试结果失败: ${error.message}`);
  931. }
  932. }
  933. /**
  934. * 批量测试节点
  935. */
  936. async testNodes(nodes) {
  937. logger.info(`开始批量测试 ${nodes.length} 个节点`);
  938. const results = [];
  939. const concurrency = parseInt(process.env.CONCURRENCY) || 5;
  940. // 分批处理,控制并发数
  941. for (let i = 0; i < nodes.length; i += concurrency) {
  942. const batch = nodes.slice(i, i + concurrency);
  943. const batchPromises = batch.map(node => this.testNode(node));
  944. const batchResults = await Promise.allSettled(batchPromises);
  945. batchResults.forEach((result, index) => {
  946. if (result.status === 'fulfilled') {
  947. results.push(result.value);
  948. } else {
  949. logger.error(`节点测试失败: ${batch[index].name} - ${result.reason}`);
  950. }
  951. });
  952. // 批次间延迟,避免过于频繁的请求
  953. if (i + concurrency < nodes.length) {
  954. await new Promise(resolve => setTimeout(resolve, 1000));
  955. }
  956. }
  957. const successCount = results.filter(r => r.isSuccess).length;
  958. logger.info(`批量测试完成: ${successCount}/${nodes.length} 个节点连通`);
  959. return results;
  960. }
  961. /**
  962. * 数组分块
  963. */
  964. chunkArray(array, size) {
  965. const chunks = [];
  966. for (let i = 0; i < array.length; i += size) {
  967. chunks.push(array.slice(i, i + size));
  968. }
  969. return chunks;
  970. }
  971. }
  972. module.exports = SpeedTester;