@chenpu17/tcpmapping 2.1.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -2,13 +2,63 @@ const net = require('net');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
+ const crypto = require('crypto');
5
6
  const express = require('express');
6
7
  const session = require('express-session');
7
8
  const bcrypt = require('bcryptjs');
8
- const bodyParser = require('body-parser');
9
9
 
10
10
  // 数据目录 - 用于存储用户配置(持久化)
11
- const DATA_DIR = path.join(os.homedir(), '.tcpmapping');
11
+ const DATA_DIR = process.env.TCPMAPPING_DATA_DIR || path.join(os.homedir(), '.tcpmapping');
12
+ const PUBLIC_DIR = path.join(__dirname, 'public');
13
+ const SESSION_NAME = 'tcpmapping.sid';
14
+ const VERSION = require('./package.json').version;
15
+
16
+ // 默认认证策略(仅在认证配置缺少对应字段时补齐,不会创建默认用户)
17
+ const DEFAULT_PASSWORD_POLICY = {
18
+ minLength: 8,
19
+ requireUppercase: true,
20
+ requireLowercase: true,
21
+ requireNumbers: true,
22
+ requireSpecialChars: false,
23
+ passwordExpireDays: 90
24
+ };
25
+
26
+ const DEFAULT_SESSION_CONFIG = {
27
+ sessionTimeout: 24,
28
+ maxFailedAttempts: 5,
29
+ lockoutDuration: 30
30
+ };
31
+
32
+ // 日志级别定义
33
+ const LOG_LEVELS = {
34
+ error: 0,
35
+ warn: 1,
36
+ info: 2,
37
+ debug: 3
38
+ };
39
+
40
+ // 全局变量
41
+ let config = {};
42
+ const servers = new Map(); // key(host:port) -> { server, mapping }
43
+ const connections = new Map(); // 连接ID -> 连接信息(可序列化)
44
+ const connectionSockets = new Map(); // 连接ID -> { clientSocket, targetSocket, finalize }
45
+ const mappingStats = new Map(); // 映射ID -> 累计统计
46
+ const connectionHistory = []; // 最近关闭的连接记录(有界)
47
+ const pendingStarts = new Set(); // 正在启动的映射
48
+ const loginAttempts = new Map(); // IP -> { count, lastAttempt }
49
+ const processTraffic = { received: 0, sent: 0 }; // 进程启动以来的累计流量
50
+
51
+ const CONNECTION_HISTORY_LIMIT = 500; // 最多保留的已关闭连接记录数
52
+
53
+ // 认证配置
54
+ let authConfig = null;
55
+
56
+ // 用于防止通过响应时间枚举用户名
57
+ let dummyPasswordHash = null;
58
+
59
+ // HTTP 服务实例
60
+ let httpServer = null;
61
+ let shuttingDown = false;
12
62
 
13
63
  // 获取配置文件路径(按优先级搜索)
14
64
  function getConfigPath(filename) {
@@ -49,107 +99,143 @@ function getConfigSavePath(filename) {
49
99
  // 确保数据目录存在
50
100
  function ensureDataDir() {
51
101
  if (!fs.existsSync(DATA_DIR)) {
52
- fs.mkdirSync(DATA_DIR, { recursive: true });
53
- console.log(`创建配置目录: ${DATA_DIR}`);
102
+ fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
54
103
  }
55
104
  }
56
105
 
57
- // 全局变量
58
- let config = {};
59
- const servers = new Map(); // 存储所有TCP转发服务器实例
60
- const connections = new Map(); // 存储活跃连接信息
61
- const app = express();
106
+ // 原子写入文件,避免写入过程中崩溃导致配置损坏
107
+ function writeFileAtomic(filePath, data, mode) {
108
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
109
+ const options = mode ? { mode } : undefined;
110
+ fs.writeFileSync(tmpPath, data, options);
111
+ try {
112
+ fs.renameSync(tmpPath, filePath);
113
+ } catch (error) {
114
+ try {
115
+ fs.unlinkSync(tmpPath);
116
+ } catch (e) {
117
+ // 忽略清理失败
118
+ }
119
+ throw error;
120
+ }
121
+ }
62
122
 
63
- // 认证配置
64
- let authConfig = {};
123
+ // 查找认证配置文件(仅限用户目录,不使用包安装目录)
124
+ function findAuthConfigPath() {
125
+ const candidates = [
126
+ path.join(process.cwd(), 'auth-config.json'),
127
+ path.join(DATA_DIR, 'auth-config.json')
128
+ ];
65
129
 
66
- // 登录失败记录
67
- const loginAttempts = new Map();
130
+ for (const candidate of candidates) {
131
+ if (fs.existsSync(candidate)) {
132
+ return candidate;
133
+ }
134
+ }
135
+
136
+ return null;
137
+ }
68
138
 
69
139
  // 加载认证配置
70
140
  function loadAuthConfig() {
141
+ const authConfigPath = findAuthConfigPath();
142
+ if (!authConfigPath) {
143
+ console.error('警告: 未找到 auth-config.json,WebUI 管理界面将不可用');
144
+ console.error('请运行 "tcpmapping setup-password" 或 "npm run setup-password" 设置管理员密码');
145
+ return null;
146
+ }
147
+
71
148
  try {
72
- const authConfigPath = getConfigPath('auth-config.json');
73
- if (!authConfigPath) {
74
- throw new Error('认证配置文件不存在');
149
+ const parsed = JSON.parse(fs.readFileSync(authConfigPath, 'utf8'));
150
+
151
+ if (!Array.isArray(parsed.users) || parsed.users.length === 0) {
152
+ console.error(`警告: 认证配置 ${authConfigPath} 中没有有效用户,WebUI 管理界面将不可用`);
153
+ return null;
75
154
  }
76
- const authConfigData = fs.readFileSync(authConfigPath, 'utf8');
155
+
156
+ // 补齐策略配置,兼容旧版本配置
157
+ parsed.passwordPolicy = { ...DEFAULT_PASSWORD_POLICY, ...(parsed.passwordPolicy || {}) };
158
+ parsed.sessionConfig = { ...DEFAULT_SESSION_CONFIG, ...(parsed.sessionConfig || {}) };
159
+
77
160
  console.log(`加载认证配置: ${authConfigPath}`);
78
- return JSON.parse(authConfigData);
161
+ return parsed;
79
162
  } catch (error) {
80
- console.error('警告: 未找到认证配置文件,使用默认配置');
81
- console.error('请运行 "tcpmapping setup-password" 或 "npm run setup-password" 来设置安全密码');
82
-
83
- // 返回默认配置(仅用于向后兼容)
84
- return {
85
- users: [{
86
- username: 'admin',
87
- passwordHash: bcrypt.hashSync('admin123', 10),
88
- role: 'admin',
89
- lastPasswordChange: new Date().toISOString(),
90
- enabled: true
91
- }],
92
- passwordPolicy: {
93
- minLength: 8,
94
- requireUppercase: true,
95
- requireLowercase: true,
96
- requireNumbers: true,
97
- requireSpecialChars: false,
98
- passwordExpireDays: 90
99
- },
100
- sessionConfig: {
101
- sessionTimeout: 24,
102
- maxFailedAttempts: 5,
103
- lockoutDuration: 30
104
- }
105
- };
163
+ console.error(`警告: 读取认证配置失败 (${error.message}),WebUI 管理界面将不可用`);
164
+ return null;
106
165
  }
107
166
  }
108
167
 
109
168
  // 验证用户密码
110
169
  function validateUserCredentials(username, password) {
111
- const user = authConfig.users.find(u => u.username === username && u.enabled);
112
-
113
- if (!user) {
114
- return { success: false, message: '用户不存在或已被禁用' };
170
+ if (!authConfig || !Array.isArray(authConfig.users)) {
171
+ return { success: false, message: '系统尚未配置管理员密码,请运行 tcpmapping setup-password' };
172
+ }
173
+
174
+ const user = authConfig.users.find(u => u.username === username);
175
+ const hash = user && typeof user.passwordHash === 'string'
176
+ ? user.passwordHash
177
+ : dummyPasswordHash;
178
+
179
+ let passwordMatches = false;
180
+ try {
181
+ passwordMatches = bcrypt.compareSync(password || '', hash);
182
+ } catch (error) {
183
+ passwordMatches = false;
115
184
  }
116
-
185
+
186
+ // 统一提示,避免通过错误信息枚举用户名
187
+ if (!user || user.enabled === false || !passwordMatches) {
188
+ return { success: false, message: '用户名或密码错误' };
189
+ }
190
+
117
191
  // 检查密码是否过期
118
- const passwordAge = Date.now() - new Date(user.lastPasswordChange).getTime();
119
- const passwordExpireMs = authConfig.passwordPolicy.passwordExpireDays * 24 * 60 * 60 * 1000;
120
-
121
- if (passwordAge > passwordExpireMs) {
122
- return { success: false, message: '密码已过期,请联系管理员重置' };
123
- }
124
-
125
- // 验证密码
126
- if (bcrypt.compareSync(password, user.passwordHash)) {
127
- return { success: true, user: user };
128
- } else {
129
- return { success: false, message: '密码错误' };
192
+ const expireDays = Number(authConfig.passwordPolicy.passwordExpireDays) || 0;
193
+ if (expireDays > 0) {
194
+ const changedAt = Date.parse(user.lastPasswordChange);
195
+ if (!Number.isFinite(changedAt)) {
196
+ return { success: false, message: '密码状态异常,请运行 tcpmapping setup-password 重新设置密码' };
197
+ }
198
+ if (Date.now() - changedAt > expireDays * 24 * 60 * 60 * 1000) {
199
+ return { success: false, message: `密码已超过 ${expireDays} 天未修改,请运行 tcpmapping setup-password 重置` };
200
+ }
201
+ }
202
+
203
+ return { success: true, user: user };
204
+ }
205
+
206
+ // 清理过期的登录失败记录
207
+ function pruneLoginAttempts() {
208
+ const lockoutTime = (authConfig ? authConfig.sessionConfig.lockoutDuration : 30) * 60 * 1000;
209
+ const now = Date.now();
210
+ for (const [ip, attempts] of loginAttempts) {
211
+ if (now - attempts.lastAttempt > lockoutTime * 2) {
212
+ loginAttempts.delete(ip);
213
+ }
130
214
  }
131
215
  }
132
216
 
133
217
  // 检查登录尝试限制
134
218
  function checkLoginAttempts(clientIP) {
219
+ pruneLoginAttempts();
220
+
135
221
  const attempts = loginAttempts.get(clientIP) || { count: 0, lastAttempt: 0 };
222
+ const sessionConfig = authConfig ? authConfig.sessionConfig : DEFAULT_SESSION_CONFIG;
136
223
  const now = Date.now();
137
- const lockoutTime = authConfig.sessionConfig.lockoutDuration * 60 * 1000;
138
-
139
- // 如果在锁定期内
140
- if (attempts.count >= authConfig.sessionConfig.maxFailedAttempts) {
224
+ const lockoutTime = sessionConfig.lockoutDuration * 60 * 1000;
225
+
226
+ if (attempts.count >= sessionConfig.maxFailedAttempts) {
141
227
  if (now - attempts.lastAttempt < lockoutTime) {
142
228
  const remainingTime = Math.ceil((lockoutTime - (now - attempts.lastAttempt)) / 1000 / 60);
143
- return {
144
- allowed: false,
145
- message: `由于多次登录失败,账户已被锁定,请${remainingTime}分钟后再试`
229
+ return {
230
+ allowed: false,
231
+ message: `由于多次登录失败,账户已被锁定,请${remainingTime}分钟后再试`
146
232
  };
147
- } else {
148
- // 锁定期已过,重置计数
149
- loginAttempts.set(clientIP, { count: 0, lastAttempt: now });
150
233
  }
234
+
235
+ // 锁定期已过,重置计数
236
+ loginAttempts.set(clientIP, { count: 0, lastAttempt: now });
151
237
  }
152
-
238
+
153
239
  return { allowed: true };
154
240
  }
155
241
 
@@ -166,45 +252,81 @@ function clearLoginFailures(clientIP) {
166
252
  loginAttempts.delete(clientIP);
167
253
  }
168
254
 
169
- // 日志级别定义
170
- const LOG_LEVELS = {
171
- error: 0,
172
- warn: 1,
173
- info: 2,
174
- debug: 3
175
- };
255
+ // 使用环境变量覆盖 WebUI 监听配置
256
+ function applyEnvOverrides(loadedConfig) {
257
+ if (process.env.WEBUI_PORT && isValidPort(process.env.WEBUI_PORT)) {
258
+ loadedConfig.webui.port = Number(process.env.WEBUI_PORT);
259
+ }
260
+ if (process.env.WEBUI_HOST && isValidHost(process.env.WEBUI_HOST)) {
261
+ loadedConfig.webui.host = process.env.WEBUI_HOST;
262
+ }
263
+ return loadedConfig;
264
+ }
176
265
 
177
266
  // 读取配置文件
178
267
  function loadConfig() {
268
+ const defaults = getDefaultConfig();
269
+
270
+ let configPath = null;
179
271
  try {
180
- const configPath = getConfigPath('config.json');
272
+ configPath = getConfigPath('config.json');
181
273
  if (!configPath) {
182
274
  console.log('未找到配置文件,使用默认配置');
183
275
  console.log('运行 "tcpmapping init" 创建配置文件');
184
- return getDefaultConfig();
276
+ return applyEnvOverrides(defaults);
185
277
  }
186
- const configData = fs.readFileSync(configPath, 'utf8');
187
- const loadedConfig = JSON.parse(configData);
278
+
279
+ const loadedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
188
280
  console.log(`加载配置文件: ${configPath}`);
189
281
 
190
- // 验证配置
191
- if (!loadedConfig.mappings || !Array.isArray(loadedConfig.mappings)) {
282
+ if (!Array.isArray(loadedConfig.mappings)) {
192
283
  loadedConfig.mappings = [];
193
284
  }
194
285
 
195
- // 添加默认WebUI配置
196
- if (!loadedConfig.webui) {
197
- loadedConfig.webui = {
198
- enabled: true,
199
- port: 9999,
200
- host: '0.0.0.0'
201
- };
202
- }
286
+ // 规范映射数据,补齐缺失字段
287
+ loadedConfig.mappings = loadedConfig.mappings
288
+ .filter(mapping => mapping && typeof mapping === 'object')
289
+ .map((mapping, index) => {
290
+ if (!mapping.name || typeof mapping.name !== 'string') {
291
+ mapping.name = `映射${index + 1}`;
292
+ }
293
+ if (!mapping.listen || typeof mapping.listen !== 'object') {
294
+ mapping.listen = { host: '0.0.0.0', port: 0 };
295
+ } else {
296
+ mapping.listen.host = String(mapping.listen.host || '0.0.0.0').trim();
297
+ mapping.listen.port = Number(mapping.listen.port);
298
+ }
299
+ if (!mapping.target || typeof mapping.target !== 'object') {
300
+ mapping.target = { host: '', port: 0 };
301
+ } else {
302
+ mapping.target.host = String(mapping.target.host || '').trim();
303
+ mapping.target.port = Number(mapping.target.port);
304
+ }
305
+ if (!mapping.id || typeof mapping.id !== 'string') {
306
+ mapping.id = generateMappingId();
307
+ }
308
+ return mapping;
309
+ });
310
+
311
+ // 提示重复的监听地址和端口
312
+ const seenListen = new Set();
313
+ loadedConfig.mappings.forEach(mapping => {
314
+ const key = `${mapping.listen.host}:${mapping.listen.port}`;
315
+ if (seenListen.has(key)) {
316
+ console.warn(`警告: 检测到重复的监听地址 ${key},仅第一个会生效,请检查配置文件`);
317
+ }
318
+ seenListen.add(key);
319
+ });
203
320
 
204
- return loadedConfig;
321
+ loadedConfig.webui = { ...defaults.webui, ...(loadedConfig.webui || {}) };
322
+ loadedConfig.logging = { ...defaults.logging, ...(loadedConfig.logging || {}) };
323
+
324
+ // 支持通过环境变量覆盖 WebUI 监听配置
325
+ return applyEnvOverrides(loadedConfig);
205
326
  } catch (error) {
206
- console.error('读取配置文件失败:', error.message);
207
- return getDefaultConfig();
327
+ console.error(`读取配置文件失败 (${configPath || 'config.json'}):`, error.message);
328
+ console.error('已使用默认配置继续运行,原文件不会被覆盖');
329
+ return applyEnvOverrides(defaults);
208
330
  }
209
331
  }
210
332
 
@@ -215,11 +337,16 @@ function getDefaultConfig() {
215
337
  webui: {
216
338
  enabled: true,
217
339
  port: 9999,
218
- host: '0.0.0.0'
340
+ // 默认只监听本机,避免管理面板暴露到公网
341
+ host: '127.0.0.1'
219
342
  },
220
343
  logging: {
221
344
  enabled: true,
222
345
  level: 'info'
346
+ },
347
+ timeouts: {
348
+ connect: 30000,
349
+ idle: 300000
223
350
  }
224
351
  };
225
352
  }
@@ -228,7 +355,7 @@ function getDefaultConfig() {
228
355
  function saveConfig(newConfig) {
229
356
  try {
230
357
  const configPath = getConfigSavePath('config.json');
231
- fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
358
+ writeFileAtomic(configPath, JSON.stringify(newConfig, null, 2));
232
359
  config = newConfig;
233
360
  console.log(`配置已保存: ${configPath}`);
234
361
  return true;
@@ -240,56 +367,267 @@ function saveConfig(newConfig) {
240
367
 
241
368
  // 日志输出函数
242
369
  function log(level, message) {
243
- if (config.logging && config.logging.enabled) {
244
- const configLevel = config.logging.level || 'info';
245
- const currentLevelValue = LOG_LEVELS[level] || 999;
246
- const configLevelValue = LOG_LEVELS[configLevel] || 2;
247
-
248
- // 只有当前日志级别的优先级 <= 配置级别优先级时才输出
249
- if (currentLevelValue <= configLevelValue) {
250
- const timestamp = new Date().toISOString();
251
- console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
252
- }
370
+ if (!config.logging || config.logging.enabled === false) {
371
+ return;
372
+ }
373
+
374
+ const configLevel = config.logging.level || 'info';
375
+ const currentLevelValue = LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : 999;
376
+ const configLevelValue = LOG_LEVELS[configLevel] !== undefined ? LOG_LEVELS[configLevel] : 2;
377
+
378
+ // 只有当前日志级别的优先级 <= 配置级别优先级时才输出
379
+ if (currentLevelValue <= configLevelValue) {
380
+ const timestamp = new Date().toISOString();
381
+ console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
253
382
  }
254
383
  }
255
384
 
256
385
  // 生成连接ID
257
386
  function generateConnectionId() {
258
- return Math.random().toString(36).substring(2, 15);
387
+ return crypto.randomBytes(8).toString('hex');
259
388
  }
260
389
 
261
390
  // 生成映射唯一ID
262
391
  function generateMappingId() {
263
- return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
392
+ return crypto.randomBytes(8).toString('hex');
393
+ }
394
+
395
+ // 格式化 host:port,IPv6 地址自动加方括号
396
+ function formatEndpoint(host, port) {
397
+ if (!host) return `:${port}`;
398
+ const value = String(host);
399
+ return value.includes(':') ? `[${value}]:${port}` : `${value}:${port}`;
400
+ }
401
+
402
+ // 格式化毫秒时长为易读文本
403
+ function formatDurationMs(ms) {
404
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
405
+ if (totalSeconds < 60) return `${totalSeconds}s`;
406
+ if (totalSeconds < 3600) return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
407
+ if (totalSeconds < 86400) return `${Math.floor(totalSeconds / 3600)}h ${Math.floor((totalSeconds % 3600) / 60)}m`;
408
+ return `${Math.floor(totalSeconds / 86400)}d ${Math.floor((totalSeconds % 86400) / 3600)}h`;
409
+ }
410
+
411
+ // ==================== 服务自身资源采样 ====================
412
+
413
+ const CPU_COUNT = Math.max(1, (os.cpus() || []).length);
414
+ const resourceSnapshot = {
415
+ cpuPercent: 0,
416
+ memory: {
417
+ rss: 0,
418
+ heapTotal: 0,
419
+ heapUsed: 0,
420
+ external: 0,
421
+ arrayBuffers: 0,
422
+ systemTotal: os.totalmem(),
423
+ systemFree: os.freemem()
424
+ },
425
+ traffic: {
426
+ receivedPerSecond: 0,
427
+ sentPerSecond: 0,
428
+ totalReceived: 0,
429
+ totalSent: 0
430
+ },
431
+ sampledAt: null
432
+ };
433
+
434
+ let lastCpuUsage = process.cpuUsage();
435
+ let lastCpuSampleTime = process.hrtime.bigint();
436
+ let lastTrafficSample = { received: 0, sent: 0, time: Date.now() };
437
+
438
+ // 每秒采样一次;CPU 百分比按整机核数归一化(0-100)
439
+ function sampleProcessResources() {
440
+ const nowHr = process.hrtime.bigint();
441
+ const elapsedUs = Number(nowHr - lastCpuSampleTime) / 1000;
442
+ const usage = process.cpuUsage(lastCpuUsage);
443
+ lastCpuUsage = process.cpuUsage();
444
+ lastCpuSampleTime = nowHr;
445
+
446
+ const cpuUs = usage.user + usage.system;
447
+ const cpuPercent = elapsedUs > 0
448
+ ? Math.min(100, Math.max(0, (cpuUs / elapsedUs) * 100 / CPU_COUNT))
449
+ : 0;
450
+
451
+ const mem = process.memoryUsage();
452
+ const now = Date.now();
453
+ const elapsedSec = (now - lastTrafficSample.time) / 1000;
454
+ let receivedPerSecond = 0;
455
+ let sentPerSecond = 0;
456
+
457
+ if (elapsedSec > 0) {
458
+ receivedPerSecond = Math.max(0, (processTraffic.received - lastTrafficSample.received) / elapsedSec);
459
+ sentPerSecond = Math.max(0, (processTraffic.sent - lastTrafficSample.sent) / elapsedSec);
460
+ }
461
+
462
+ lastTrafficSample = { received: processTraffic.received, sent: processTraffic.sent, time: now };
463
+
464
+ resourceSnapshot.cpuPercent = cpuPercent;
465
+ resourceSnapshot.memory = {
466
+ rss: mem.rss,
467
+ heapTotal: mem.heapTotal,
468
+ heapUsed: mem.heapUsed,
469
+ external: mem.external,
470
+ arrayBuffers: mem.arrayBuffers,
471
+ systemTotal: os.totalmem(),
472
+ systemFree: os.freemem()
473
+ };
474
+ resourceSnapshot.traffic = {
475
+ receivedPerSecond,
476
+ sentPerSecond,
477
+ totalReceived: processTraffic.received,
478
+ totalSent: processTraffic.sent
479
+ };
480
+ resourceSnapshot.sampledAt = new Date(now).toISOString();
481
+ }
482
+
483
+ sampleProcessResources();
484
+ setInterval(sampleProcessResources, 1000).unref();
485
+
486
+ // API 英文文案(用于 WebUI 切换到英文时)
487
+ const API_MESSAGES_EN = {
488
+ '需要登录': 'Authentication required',
489
+ '非法请求来源': 'Invalid request origin',
490
+ '跨站请求被拒绝': 'Cross-site request rejected',
491
+ '请输入用户名和密码': 'Please enter username and password',
492
+ '用户名或密码错误': 'Invalid username or password',
493
+ '登录成功': 'Login successful',
494
+ '已登出': 'Logged out',
495
+ '登录失败,请稍后重试': 'Login failed, please try again later',
496
+ '系统尚未配置管理员密码,请运行 tcpmapping setup-password': 'Administrator password is not configured. Run tcpmapping setup-password first.',
497
+ '系统尚未配置管理员密码,请先在服务器上运行 tcpmapping setup-password': 'Administrator password is not configured. Run tcpmapping setup-password on the server first.',
498
+ '密码状态异常,请运行 tcpmapping setup-password 重新设置密码': 'Password state is invalid. Run tcpmapping setup-password to reset it.',
499
+ '映射不存在': 'Mapping not found',
500
+ '该监听地址和端口已存在映射': 'A mapping with this listen address and port already exists',
501
+ '保存配置失败': 'Failed to save configuration',
502
+ '端口映射已启动': 'Port mapping started',
503
+ '端口映射已停止': 'Port mapping stopped',
504
+ '端口映射已在运行': 'Port mapping is already running',
505
+ '端口映射正在启动,请稍后重试': 'Port mapping is starting, please try again later',
506
+ '端口映射不存在或未在运行': 'Port mapping does not exist or is not running',
507
+ '端口映射已更新': 'Port mapping updated',
508
+ '端口映射已删除': 'Port mapping deleted',
509
+ '接口不存在': 'API endpoint not found',
510
+ '服务器内部错误': 'Internal server error',
511
+ '映射名称不能为空': 'Mapping name is required',
512
+ '映射名称不能超过 100 个字符': 'Mapping name must not exceed 100 characters',
513
+ '监听端口必须在 1-65535 范围内': 'Listen port must be between 1 and 65535',
514
+ '监听地址格式无效': 'Invalid listen address',
515
+ '目标主机地址格式无效': 'Invalid target host address',
516
+ '目标端口必须在 1-65535 范围内': 'Target port must be between 1 and 65535'
517
+ };
518
+
519
+ // 动态消息(含变量)的英文转换
520
+ const API_MESSAGE_PATTERNS_EN = [
521
+ [/^密码已超过 (\d+) 天未修改,请运行 tcpmapping setup-password 重置$/,
522
+ (match, days) => `Password has not been changed for ${days} days. Run tcpmapping setup-password to reset it.`],
523
+ [/^由于多次登录失败,账户已被锁定,请(\d+)分钟后再试$/,
524
+ (match, minutes) => `Account locked after multiple failed attempts. Try again in ${minutes} minute(s).`],
525
+ [/^监听端口 (.+) 已被占用$/,
526
+ (match, address) => `Port ${address} is already in use`],
527
+ [/^启动失败: (.+)$/,
528
+ (match, detail) => `Failed to start: ${detail}`]
529
+ ];
530
+
531
+ // 将 API 返回的中文提示转换为目标语言
532
+ function localizeApiMessage(message, lang) {
533
+ if (lang !== 'en' || typeof message !== 'string') {
534
+ return message;
535
+ }
536
+
537
+ if (API_MESSAGES_EN[message]) {
538
+ return API_MESSAGES_EN[message];
539
+ }
540
+
541
+ for (const [pattern, build] of API_MESSAGE_PATTERNS_EN) {
542
+ const match = message.match(pattern);
543
+ if (match) {
544
+ return build(...match);
545
+ }
546
+ }
547
+
548
+ // 校验错误使用 "; " 拼接,逐条翻译
549
+ if (message.includes('; ')) {
550
+ return message.split('; ').map(part => localizeApiMessage(part, lang)).join('; ');
551
+ }
552
+
553
+ return message;
554
+ }
555
+
556
+ // 读取请求语言(前端通过 X-Language 指定)
557
+ function getRequestLanguage(req) {
558
+ const explicit = String(req.get('X-Language') || '').toLowerCase();
559
+ if (explicit.startsWith('zh')) return 'zh';
560
+ if (explicit.startsWith('en')) return 'en';
561
+
562
+ const acceptLanguage = String(req.get('Accept-Language') || '').toLowerCase();
563
+ if (acceptLanguage.trim()) {
564
+ // Accept-Language 的第一项是客户端的首选语言
565
+ return acceptLanguage.trim().startsWith('zh') ? 'zh' : 'en';
566
+ }
567
+
568
+ return 'zh';
569
+ }
570
+
571
+ // 连接关闭原因英文对照
572
+ const CLOSE_REASONS_EN = {
573
+ '客户端关闭': 'Client closed',
574
+ '目标服务器关闭': 'Target closed',
575
+ '目标连接超时': 'Target connect timeout',
576
+ '目标连接空闲超时': 'Target idle timeout',
577
+ '客户端空闲超时': 'Client idle timeout',
578
+ '目标连接错误': 'Target connection error',
579
+ '客户端连接错误': 'Client connection error',
580
+ '端口映射已停止': 'Mapping stopped',
581
+ '服务正在关闭': 'Server shutting down',
582
+ '连接被主动断开': 'Closed by administrator'
583
+ };
584
+
585
+ function localizeCloseReason(reason, lang) {
586
+ if (lang !== 'en') {
587
+ return reason;
588
+ }
589
+ return CLOSE_REASONS_EN[reason] || reason;
264
590
  }
265
591
 
266
592
  // 验证端口号
267
593
  function isValidPort(port) {
268
- const p = parseInt(port);
594
+ const p = Number(port);
269
595
  return Number.isInteger(p) && p >= 1 && p <= 65535;
270
596
  }
271
597
 
272
598
  // 验证IP地址或主机名
273
599
  function isValidHost(host) {
274
600
  if (!host || typeof host !== 'string') return false;
275
- // 允许 0.0.0.0, localhost, IP地址, 或域名
276
- const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
601
+
602
+ const value = host.trim();
603
+ if (!value || value.length > 253) return false;
604
+
605
+ // IPv4 / IPv6
606
+ if (net.isIP(value)) return true;
607
+
608
+ // 域名
277
609
  const hostnamePattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
278
- return ipPattern.test(host) || hostnamePattern.test(host);
610
+ return hostnamePattern.test(value);
279
611
  }
280
612
 
281
613
  // 验证映射配置
282
614
  function validateMappingInput(mapping) {
283
615
  const errors = [];
284
616
 
285
- if (!mapping.name || mapping.name.trim().length === 0) {
617
+ if (!mapping.name || typeof mapping.name !== 'string' || mapping.name.trim().length === 0) {
286
618
  errors.push('映射名称不能为空');
619
+ } else if (mapping.name.trim().length > 100) {
620
+ errors.push('映射名称不能超过 100 个字符');
287
621
  }
288
622
 
289
623
  if (!mapping.listen || !isValidPort(mapping.listen.port)) {
290
624
  errors.push('监听端口必须在 1-65535 范围内');
291
625
  }
292
626
 
627
+ if (!mapping.listen || !isValidHost(mapping.listen.host)) {
628
+ errors.push('监听地址格式无效');
629
+ }
630
+
293
631
  if (!mapping.target || !isValidHost(mapping.target.host)) {
294
632
  errors.push('目标主机地址格式无效');
295
633
  }
@@ -304,22 +642,93 @@ function validateMappingInput(mapping) {
304
642
  };
305
643
  }
306
644
 
307
- // 默认连接超时(毫秒)
308
- const CONNECTION_TIMEOUT = 30000;
309
- const IDLE_TIMEOUT = 300000; // 5分钟空闲超时
645
+ // 读取超时配置(毫秒)
646
+ function getTimeouts() {
647
+ const defaults = getDefaultConfig().timeouts;
648
+ const timeouts = config.timeouts || {};
649
+
650
+ const connect = Number(timeouts.connect);
651
+ const idle = Number(timeouts.idle);
652
+
653
+ return {
654
+ // 连接超时至少 1 秒
655
+ connect: Number.isFinite(connect) && connect > 0 ? Math.max(1000, connect) : defaults.connect,
656
+ // 0 表示禁用空闲超时
657
+ idle: Number.isFinite(idle) && idle >= 0 ? idle : defaults.idle
658
+ };
659
+ }
660
+
661
+ // 获取映射的累计统计
662
+ function getMappingStats(mappingId) {
663
+ let stats = mappingStats.get(mappingId);
664
+ if (!stats) {
665
+ stats = {
666
+ totalConnections: 0,
667
+ bytesReceived: 0,
668
+ bytesSent: 0
669
+ };
670
+ mappingStats.set(mappingId, stats);
671
+ }
672
+ return stats;
673
+ }
674
+
675
+ // 主动销毁一条转发连接
676
+ function destroyConnection(connectionId, reason) {
677
+ const pair = connectionSockets.get(connectionId);
678
+
679
+ // 走统一的收尾逻辑,确保记录持续时长与关闭原因
680
+ if (pair && typeof pair.finalize === 'function') {
681
+ pair.finalize(reason || '连接被主动断开', 'info');
682
+ return;
683
+ }
684
+
685
+ connections.delete(connectionId);
686
+ connectionSockets.delete(connectionId);
687
+
688
+ if (!pair) {
689
+ return;
690
+ }
691
+
692
+ if (pair.clientSocket && !pair.clientSocket.destroyed) {
693
+ try {
694
+ pair.clientSocket.destroy();
695
+ } catch (error) {
696
+ // 忽略销毁错误
697
+ }
698
+ }
699
+
700
+ if (pair.targetSocket && !pair.targetSocket.destroyed) {
701
+ try {
702
+ pair.targetSocket.destroy();
703
+ } catch (error) {
704
+ // 忽略销毁错误
705
+ }
706
+ }
707
+ }
310
708
 
311
709
  // 创建转发连接
312
710
  function createForwardConnection(clientSocket, mapping) {
313
711
  const connectionId = generateConnectionId();
314
- const clientInfo = `${clientSocket.remoteAddress}:${clientSocket.remotePort}`;
712
+ const clientInfo = formatEndpoint(clientSocket.remoteAddress, clientSocket.remotePort);
713
+ const targetAddress = formatEndpoint(mapping.target.host, mapping.target.port);
714
+ // clientSocket.localAddress 是实际命中的网卡地址,比 0.0.0.0 更有参考价值
715
+ const listenAddress = clientSocket.localAddress
716
+ ? formatEndpoint(clientSocket.localAddress, clientSocket.localPort)
717
+ : formatEndpoint(mapping.listen.host, mapping.listen.port);
718
+ const stats = getMappingStats(mapping.id);
719
+
720
+ stats.totalConnections++;
315
721
 
316
722
  // 记录连接信息
317
723
  connections.set(connectionId, {
318
724
  id: connectionId,
725
+ mappingId: mapping.id,
319
726
  mappingName: mapping.name,
727
+ sourceHost: mapping.listen.host,
320
728
  sourcePort: mapping.listen.port,
729
+ listenAddress: listenAddress,
321
730
  clientAddress: clientInfo,
322
- targetAddress: `${mapping.target.host}:${mapping.target.port}`,
731
+ targetAddress: targetAddress,
323
732
  startTime: new Date(),
324
733
  lastActivity: Date.now(),
325
734
  bytesReceived: 0,
@@ -327,16 +736,19 @@ function createForwardConnection(clientSocket, mapping) {
327
736
  status: 'connecting'
328
737
  });
329
738
 
330
- const targetSocket = new net.Socket();
739
+ const targetSocket = new net.Socket({ allowHalfOpen: true });
740
+ const pair = { clientSocket, targetSocket, finalize: null };
741
+ connectionSockets.set(connectionId, pair);
331
742
 
332
- // 设置连接超时
333
- targetSocket.setTimeout(CONNECTION_TIMEOUT);
743
+ const timeouts = getTimeouts();
744
+ let finished = false;
334
745
 
335
- // 设置socket超时(空闲超时)
336
- clientSocket.setTimeout(IDLE_TIMEOUT);
337
- targetSocket.setTimeout(IDLE_TIMEOUT);
746
+ // 降低转发延迟,并启用 TCP KeepAlive 检测死连接
747
+ clientSocket.setNoDelay(true);
748
+ targetSocket.setNoDelay(true);
749
+ clientSocket.setKeepAlive(true, 60000);
750
+ targetSocket.setKeepAlive(true, 60000);
338
751
 
339
- // 更新活动时间
340
752
  const updateActivity = () => {
341
753
  const conn = connections.get(connectionId);
342
754
  if (conn) {
@@ -344,151 +756,244 @@ function createForwardConnection(clientSocket, mapping) {
344
756
  }
345
757
  };
346
758
 
347
- // 连接到目标服务器
348
- targetSocket.connect(mapping.target.port, mapping.target.host, () => {
349
- log('info', `[${mapping.name}] 已连接到目标服务器 ${mapping.target.host}:${mapping.target.port}`);
759
+ // 统一收尾:只执行一次,避免重复销毁
760
+ const finalize = (reason, level) => {
761
+ if (finished) return;
762
+ finished = true;
763
+
764
+ const conn = connections.get(connectionId);
765
+
766
+ connections.delete(connectionId);
767
+ connectionSockets.delete(connectionId);
768
+
769
+ // 记录已关闭连接的摘要,便于事后查询源地址、目的地址与持续时长
770
+ if (conn) {
771
+ const endTime = new Date();
772
+ const durationMs = Math.max(0, endTime.getTime() - conn.startTime.getTime());
773
+
774
+ connectionHistory.push({
775
+ id: conn.id,
776
+ mappingId: conn.mappingId,
777
+ mappingName: conn.mappingName,
778
+ listenAddress: conn.listenAddress,
779
+ clientAddress: conn.clientAddress,
780
+ targetAddress: conn.targetAddress,
781
+ startTime: conn.startTime,
782
+ endTime: endTime,
783
+ durationMs: durationMs,
784
+ bytesReceived: conn.bytesReceived,
785
+ bytesSent: conn.bytesSent,
786
+ status: conn.status,
787
+ closeReason: reason
788
+ });
789
+
790
+ if (connectionHistory.length > CONNECTION_HISTORY_LIMIT) {
791
+ connectionHistory.splice(0, connectionHistory.length - CONNECTION_HISTORY_LIMIT);
792
+ }
793
+
794
+ log(level || 'info',
795
+ `[${mapping.name}] 连接关闭: ${clientInfo} -> ${targetAddress},` +
796
+ `持续 ${formatDurationMs(durationMs)},↑${conn.bytesReceived}B ↓${conn.bytesSent}B (${reason})`);
797
+ } else {
798
+ log(level || 'info', `[${mapping.name}] 连接关闭: ${clientInfo} (${reason})`);
799
+ }
800
+
801
+ if (!clientSocket.destroyed) clientSocket.destroy();
802
+ if (!targetSocket.destroyed) targetSocket.destroy();
803
+ };
804
+
805
+ pair.finalize = finalize;
806
+
807
+ // 连接超时 / 空闲超时
808
+ if (timeouts.connect > 0) {
809
+ targetSocket.setTimeout(timeouts.connect);
810
+ }
811
+
812
+ targetSocket.once('connect', () => {
350
813
  const conn = connections.get(connectionId);
351
814
  if (conn) {
352
815
  conn.status = 'connected';
353
- connections.set(connectionId, conn);
816
+ conn.lastActivity = Date.now();
817
+ }
818
+
819
+ log('info', `[${mapping.name}] 已连接到目标服务器 ${targetAddress}`);
820
+
821
+ if (timeouts.idle > 0) {
822
+ targetSocket.setTimeout(timeouts.idle);
823
+ } else {
824
+ targetSocket.setTimeout(0);
354
825
  }
355
826
  });
356
827
 
357
- // 处理超时
828
+ clientSocket.setTimeout(timeouts.idle > 0 ? timeouts.idle : 0);
829
+
358
830
  targetSocket.on('timeout', () => {
359
- log('warn', `[${mapping.name}] 目标连接超时`);
360
- connections.delete(connectionId);
361
- targetSocket.destroy();
362
- clientSocket.destroy();
831
+ const conn = connections.get(connectionId);
832
+ const message = conn && conn.status === 'connecting' ? '目标连接超时' : '目标连接空闲超时';
833
+ log('warn', `[${mapping.name}] ${message}: ${targetAddress}`);
834
+ finalize(message, 'warn');
363
835
  });
364
836
 
365
837
  clientSocket.on('timeout', () => {
366
- log('warn', `[${mapping.name}] 客户端连接空闲超时`);
367
- connections.delete(connectionId);
368
- targetSocket.destroy();
369
- clientSocket.destroy();
838
+ log('warn', `[${mapping.name}] 客户端连接空闲超时: ${clientInfo}`);
839
+ finalize('客户端空闲超时', 'warn');
370
840
  });
371
841
 
372
- // 处理目标服务器连接错误
842
+ // 错误处理
373
843
  targetSocket.on('error', (err) => {
374
- log('error', `[${mapping.name}] 目标服务器连接错误: ${err.message}`);
375
- connections.delete(connectionId);
376
- clientSocket.destroy();
844
+ log('error', `[${mapping.name}] 目标服务器连接错误: ${err.message} (${targetAddress})`);
845
+ finalize('目标连接错误', 'error');
377
846
  });
378
847
 
379
- // 处理目标服务器连接关闭
380
- targetSocket.on('close', () => {
381
- log('info', `[${mapping.name}] 目标服务器连接已关闭`);
382
- connections.delete(connectionId);
383
- clientSocket.destroy();
848
+ clientSocket.on('error', (err) => {
849
+ log('error', `[${mapping.name}] 客户端连接错误: ${err.message} (${clientInfo})`);
850
+ finalize('客户端连接错误', 'error');
384
851
  });
385
852
 
386
- // 处理客户端连接关闭
387
- clientSocket.on('close', () => {
388
- log('info', `[${mapping.name}] 客户端连接已关闭`);
389
- connections.delete(connectionId);
390
- targetSocket.destroy();
853
+ // 关闭事件(对端正常关闭)
854
+ targetSocket.on('close', () => finalize('目标服务器关闭'));
855
+ clientSocket.on('close', () => finalize('客户端关闭'));
856
+
857
+ // 支持 TCP 半关闭:一个方向结束后仍允许另一个方向继续传输
858
+ clientSocket.on('end', () => {
859
+ if (!targetSocket.destroyed && targetSocket.writable) {
860
+ targetSocket.end();
861
+ }
391
862
  });
392
863
 
393
- // 处理客户端连接错误
394
- clientSocket.on('error', (err) => {
395
- log('error', `[${mapping.name}] 客户端连接错误: ${err.message}`);
396
- connections.delete(connectionId);
397
- targetSocket.destroy();
864
+ targetSocket.on('end', () => {
865
+ if (!clientSocket.destroyed && clientSocket.writable) {
866
+ clientSocket.end();
867
+ }
398
868
  });
399
869
 
400
- // 数据转发:客户端 -> 目标服务器
870
+ // 流量统计
401
871
  clientSocket.on('data', (data) => {
402
- log('debug', `[${mapping.name}] 转发数据到目标服务器,大小: ${data.length} 字节`);
403
872
  updateActivity();
873
+ processTraffic.received += data.length;
404
874
  const conn = connections.get(connectionId);
405
875
  if (conn) {
406
876
  conn.bytesReceived += data.length;
407
- connections.set(connectionId, conn);
408
877
  }
409
- targetSocket.write(data);
878
+ stats.bytesReceived += data.length;
410
879
  });
411
880
 
412
- // 数据转发:目标服务器 -> 客户端
413
881
  targetSocket.on('data', (data) => {
414
- log('debug', `[${mapping.name}] 转发数据到客户端,大小: ${data.length} 字节`);
415
882
  updateActivity();
883
+ processTraffic.sent += data.length;
416
884
  const conn = connections.get(connectionId);
417
885
  if (conn) {
418
886
  conn.bytesSent += data.length;
419
- connections.set(connectionId, conn);
420
887
  }
421
- clientSocket.write(data);
888
+ stats.bytesSent += data.length;
422
889
  });
423
890
 
424
- return targetSocket;
891
+ // 使用 pipe 转发,自动处理背压,避免慢速对端导致内存无限增长
892
+ clientSocket.pipe(targetSocket, { end: false });
893
+ targetSocket.pipe(clientSocket, { end: false });
894
+
895
+ // 发起连接(最后执行,确保监听器已就绪)
896
+ targetSocket.connect(mapping.target.port, mapping.target.host);
425
897
  }
426
898
 
427
899
  // 创建单个端口转发服务器
428
900
  function createForwardingServer(mapping) {
429
- const server = net.createServer((clientSocket) => {
430
- const clientInfo = `${clientSocket.remoteAddress}:${clientSocket.remotePort}`;
431
- log('info', `[${mapping.name}] 新客户端连接: ${clientInfo}`);
432
-
433
- // 创建转发连接
901
+ const server = net.createServer({ allowHalfOpen: true }, (clientSocket) => {
902
+ log('info', `[${mapping.name}] 新客户端连接: ${clientSocket.remoteAddress}:${clientSocket.remotePort}`);
434
903
  createForwardConnection(clientSocket, mapping);
435
904
  });
436
905
 
437
- // 处理服务器错误
438
- server.on('error', (err) => {
439
- log('error', `[${mapping.name}] 服务器错误: ${err.message}`);
440
- if (err.code === 'EADDRINUSE') {
441
- console.error(`端口 ${mapping.listen.port} 已被占用,请检查配置或停止占用该端口的程序`);
442
- }
443
- });
444
-
445
- // 启动服务器
446
- server.listen(mapping.listen.port, mapping.listen.host, () => {
447
- log('info', `[${mapping.name}] 转发服务器已启动,监听地址: ${mapping.listen.host}:${mapping.listen.port}`);
448
- log('info', `[${mapping.name}] 目标服务器: ${mapping.target.host}:${mapping.target.port}`);
449
- console.log(`✓ [${mapping.name}] ${mapping.listen.port} -> ${mapping.target.host}:${mapping.target.port}`);
450
- });
451
-
452
906
  return server;
453
907
  }
454
908
 
455
- // 启动端口映射
909
+ // 启动端口映射(等待监听结果后再返回)
456
910
  function startMapping(mapping) {
457
- const key = `${mapping.listen.host}:${mapping.listen.port}`;
458
-
459
- if (servers.has(key)) {
460
- return { success: false, message: '端口映射已存在' };
461
- }
911
+ return new Promise((resolve) => {
912
+ const key = `${mapping.listen.host}:${mapping.listen.port}`;
913
+
914
+ if (servers.has(key)) {
915
+ return resolve({ success: true, message: '端口映射已在运行' });
916
+ }
917
+
918
+ if (pendingStarts.has(key)) {
919
+ return resolve({ success: false, message: '端口映射正在启动,请稍后重试' });
920
+ }
921
+
922
+ pendingStarts.add(key);
462
923
 
463
- try {
464
924
  const server = createForwardingServer(mapping);
465
- servers.set(key, { server, mapping });
466
- return { success: true, message: '端口映射已启动' };
467
- } catch (error) {
468
- return { success: false, message: error.message };
469
- }
925
+ let settled = false;
926
+
927
+ const finish = (result) => {
928
+ if (settled) return;
929
+ settled = true;
930
+ pendingStarts.delete(key);
931
+ resolve(result);
932
+ };
933
+
934
+ const onListenError = (err) => {
935
+ log('error', `[${mapping.name}] 启动失败: ${err.message}`);
936
+ finish({
937
+ success: false,
938
+ message: err.code === 'EADDRINUSE'
939
+ ? `监听端口 ${mapping.listen.host}:${mapping.listen.port} 已被占用`
940
+ : `启动失败: ${err.message}`
941
+ });
942
+ };
943
+
944
+ server.once('error', onListenError);
945
+
946
+ server.once('listening', () => {
947
+ server.removeListener('error', onListenError);
948
+ server.on('error', (err) => {
949
+ log('error', `[${mapping.name}] 服务器运行错误: ${err.message}`);
950
+ servers.delete(key);
951
+ // 运行期出错后关闭监听,避免留下无法通过接口管理的端口
952
+ try {
953
+ server.close();
954
+ } catch (closeError) {
955
+ // 忽略关闭错误
956
+ }
957
+ });
958
+
959
+ servers.set(key, { server, mapping });
960
+
961
+ log('info', `[${mapping.name}] 转发服务器已启动,监听地址: ${mapping.listen.host}:${mapping.listen.port}`);
962
+ log('info', `[${mapping.name}] 目标服务器: ${mapping.target.host}:${mapping.target.port}`);
963
+ console.log(`✓ [${mapping.name}] ${mapping.listen.host}:${mapping.listen.port} -> ${mapping.target.host}:${mapping.target.port}`);
964
+
965
+ finish({ success: true, message: '端口映射已启动' });
966
+ });
967
+
968
+ try {
969
+ server.listen(mapping.listen.port, mapping.listen.host);
970
+ } catch (error) {
971
+ onListenError(error);
972
+ }
973
+ });
470
974
  }
471
975
 
472
976
  // 停止端口映射
473
977
  function stopMapping(host, port) {
474
978
  const key = `${host}:${port}`;
475
-
979
+
476
980
  if (!servers.has(key)) {
477
- return { success: false, message: '端口映射不存在' };
981
+ return { success: false, message: '端口映射不存在或未在运行' };
478
982
  }
479
983
 
480
984
  try {
481
985
  const { server } = servers.get(key);
482
- server.close();
483
986
  servers.delete(key);
484
-
485
- // 清理相关连接
987
+ server.close();
988
+
989
+ // 主动断开该映射下的所有活跃连接
486
990
  for (const [id, conn] of connections) {
487
- if (conn.sourcePort === port) {
488
- connections.delete(id);
991
+ if (conn.sourceHost === host && conn.sourcePort === port) {
992
+ destroyConnection(id, '端口映射已停止');
489
993
  }
490
994
  }
491
-
995
+
996
+ log('info', `[${host}:${port}] 端口映射已停止`);
492
997
  return { success: true, message: '端口映射已停止' };
493
998
  } catch (error) {
494
999
  return { success: false, message: error.message };
@@ -496,67 +1001,169 @@ function stopMapping(host, port) {
496
1001
  }
497
1002
 
498
1003
  // 启动所有端口映射
499
- function startAllMappings() {
500
- config.mappings.forEach((mapping, index) => {
501
- if (!mapping.listen || !mapping.target) {
502
- console.error(`映射配置 ${index + 1} 缺少必要的 listen 或 target 配置`);
503
- return;
504
- }
505
-
506
- if (!mapping.name) {
507
- mapping.name = `映射${index + 1}`;
1004
+ async function startAllMappings() {
1005
+ for (let index = 0; index < config.mappings.length; index++) {
1006
+ const mapping = config.mappings[index];
1007
+
1008
+ if (!mapping.listen || !mapping.target || !isValidPort(mapping.listen.port) || !isValidPort(mapping.target.port)) {
1009
+ console.error(`映射配置 ${index + 1} 缺少必要的 listen 或 target 配置,已跳过`);
1010
+ continue;
508
1011
  }
509
1012
 
510
- startMapping(mapping);
511
- });
1013
+ const result = await startMapping(mapping);
1014
+ if (!result.success) {
1015
+ console.error(`映射配置 ${index + 1} (${mapping.name}) 启动失败: ${result.message}`);
1016
+ }
1017
+ }
512
1018
  }
513
1019
 
514
1020
  // 生成或读取 session secret
515
1021
  function getSessionSecret() {
516
- const secretPath = path.join(__dirname, '.session-secret');
1022
+ ensureDataDir();
1023
+
1024
+ const secretPath = path.join(DATA_DIR, '.session-secret');
1025
+ const legacyPath = path.join(__dirname, '.session-secret');
1026
+
1027
+ // 兼容旧版本:把包目录中的 secret 迁移到数据目录
1028
+ try {
1029
+ if (!fs.existsSync(secretPath) && fs.existsSync(legacyPath)) {
1030
+ fs.copyFileSync(legacyPath, secretPath);
1031
+ try {
1032
+ fs.chmodSync(secretPath, 0o600);
1033
+ } catch (error) {
1034
+ // Windows 上可能不支持 chmod,忽略
1035
+ }
1036
+ }
1037
+ } catch (error) {
1038
+ // 忽略迁移失败
1039
+ }
1040
+
517
1041
  try {
518
1042
  if (fs.existsSync(secretPath)) {
519
- return fs.readFileSync(secretPath, 'utf8').trim();
1043
+ const secret = fs.readFileSync(secretPath, 'utf8').trim();
1044
+ if (secret.length >= 32) {
1045
+ return secret;
1046
+ }
520
1047
  }
521
1048
  } catch (error) {
522
1049
  // 忽略读取错误
523
1050
  }
1051
+
524
1052
  // 生成新的随机 secret
525
- const crypto = require('crypto');
526
1053
  const secret = crypto.randomBytes(32).toString('hex');
527
1054
  try {
528
- fs.writeFileSync(secretPath, secret, { mode: 0o600 });
1055
+ writeFileAtomic(secretPath, secret, 0o600);
529
1056
  } catch (error) {
530
1057
  console.warn('警告: 无法保存 session secret,重启后会话将失效');
531
1058
  }
532
1059
  return secret;
533
1060
  }
534
1061
 
535
- // 中间件配置
536
- app.use(bodyParser.json());
537
- app.use(bodyParser.urlencoded({ extended: true }));
1062
+ // 加载认证配置(需在中间件创建前完成)
1063
+ authConfig = loadAuthConfig();
1064
+
1065
+ // 生成用于时序混淆的哈希
1066
+ dummyPasswordHash = bcrypt.hashSync(crypto.randomBytes(16).toString('hex'), 10);
1067
+
1068
+ const app = express();
1069
+ app.disable('x-powered-by');
1070
+
1071
+ // 安全响应头
1072
+ app.use((req, res, next) => {
1073
+ res.setHeader('X-Content-Type-Options', 'nosniff');
1074
+ res.setHeader('X-Frame-Options', 'DENY');
1075
+ res.setHeader('Referrer-Policy', 'same-origin');
1076
+ res.setHeader('Content-Security-Policy',
1077
+ "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; " +
1078
+ "img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'");
1079
+ next();
1080
+ });
1081
+
1082
+ // 请求体解析(限制大小,避免恶意大请求)
1083
+ app.use(express.json({ limit: '100kb' }));
1084
+ app.use(express.urlencoded({ extended: true, limit: '100kb' }));
1085
+
1086
+ // 根据请求语言自动本地化 API 提示信息
1087
+ app.use((req, res, next) => {
1088
+ req.lang = getRequestLanguage(req);
1089
+
1090
+ const originalJson = res.json.bind(res);
1091
+ res.json = (body) => {
1092
+ if (body && typeof body === 'object' && typeof body.message === 'string') {
1093
+ body.message = localizeApiMessage(body.message, req.lang);
1094
+ }
1095
+ return originalJson(body);
1096
+ };
1097
+
1098
+ next();
1099
+ });
1100
+
1101
+ // 会话配置
1102
+ const sessionTimeoutHours = authConfig
1103
+ ? (Number(authConfig.sessionConfig.sessionTimeout) || DEFAULT_SESSION_CONFIG.sessionTimeout)
1104
+ : DEFAULT_SESSION_CONFIG.sessionTimeout;
1105
+
538
1106
  app.use(session({
539
- secret: getSessionSecret(),
1107
+ name: SESSION_NAME,
1108
+ secret: process.env.SESSION_SECRET || getSessionSecret(),
540
1109
  resave: false,
541
1110
  saveUninitialized: false,
542
- cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24小时
1111
+ rolling: true,
1112
+ cookie: {
1113
+ httpOnly: true,
1114
+ sameSite: 'lax',
1115
+ secure: false, // 通过 HTTPS 反向代理部署时可按需改为 true
1116
+ maxAge: sessionTimeoutHours * 60 * 60 * 1000
1117
+ }
543
1118
  }));
544
1119
 
545
- // 静态文件服务
546
- app.use(express.static('public'));
1120
+ // API 响应不做缓存
1121
+ app.use('/api', (req, res, next) => {
1122
+ res.setHeader('Cache-Control', 'no-store');
1123
+ next();
1124
+ });
1125
+
1126
+ // 简单的跨站请求防护:拒绝 Origin 与 Host 不一致的写操作
1127
+ app.use('/api', (req, res, next) => {
1128
+ if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
1129
+ return next();
1130
+ }
1131
+
1132
+ const origin = req.get('origin');
1133
+ if (!origin) {
1134
+ return next();
1135
+ }
1136
+
1137
+ try {
1138
+ const originHost = new URL(origin).host.toLowerCase();
1139
+ const requestHost = String(req.get('host') || '').toLowerCase();
1140
+ if (originHost !== requestHost) {
1141
+ return res.status(403).json({ success: false, message: '跨站请求被拒绝' });
1142
+ }
1143
+ } catch (error) {
1144
+ return res.status(403).json({ success: false, message: '非法请求来源' });
1145
+ }
1146
+
1147
+ next();
1148
+ });
547
1149
 
548
- // 认证中间件
1150
+ // 认证中间件(API)
549
1151
  function requireAuth(req, res, next) {
550
1152
  if (req.session && req.session.authenticated) {
551
1153
  return next();
552
- } else {
553
- return res.status(401).json({ success: false, message: '需要登录' });
554
1154
  }
1155
+ return res.status(401).json({ success: false, message: '需要登录' });
555
1156
  }
556
1157
 
557
- // 路由定义
1158
+ // 认证中间件(页面)
1159
+ function requirePageAuth(req, res, next) {
1160
+ if (req.session && req.session.authenticated) {
1161
+ return next();
1162
+ }
1163
+ return res.redirect('/login');
1164
+ }
558
1165
 
559
- // 主页
1166
+ // 页面路由(放在静态文件之前,避免绕过登录直接访问 HTML)
560
1167
  app.get('/', (req, res) => {
561
1168
  if (req.session && req.session.authenticated) {
562
1169
  res.redirect('/dashboard');
@@ -565,303 +1172,356 @@ app.get('/', (req, res) => {
565
1172
  }
566
1173
  });
567
1174
 
568
- // 登录页面
569
1175
  app.get('/login', (req, res) => {
570
- res.sendFile(path.join(__dirname, 'public', 'login.html'));
1176
+ res.sendFile(path.join(PUBLIC_DIR, 'login.html'));
1177
+ });
1178
+
1179
+ app.get('/login.html', (req, res) => {
1180
+ res.redirect('/login');
571
1181
  });
572
1182
 
573
- // 管理面板
574
- app.get('/dashboard', requireAuth, (req, res) => {
575
- res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
1183
+ app.get('/dashboard', requirePageAuth, (req, res) => {
1184
+ res.sendFile(path.join(PUBLIC_DIR, 'dashboard.html'));
576
1185
  });
577
1186
 
1187
+ app.get('/dashboard.html', requirePageAuth, (req, res) => {
1188
+ res.redirect('/dashboard');
1189
+ });
1190
+
1191
+ // 静态文件服务(使用绝对路径,避免受启动目录影响)
1192
+ app.use(express.static(PUBLIC_DIR, { index: false, maxAge: '1h' }));
1193
+
1194
+ // 在连接列表中按映射汇总当前活跃连接
1195
+ function buildActiveStats() {
1196
+ const active = new Map();
1197
+ for (const conn of connections.values()) {
1198
+ let item = active.get(conn.mappingId);
1199
+ if (!item) {
1200
+ item = { count: 0, bytesReceived: 0, bytesSent: 0 };
1201
+ active.set(conn.mappingId, item);
1202
+ }
1203
+ item.count++;
1204
+ item.bytesReceived += conn.bytesReceived;
1205
+ item.bytesSent += conn.bytesSent;
1206
+ }
1207
+ return active;
1208
+ }
1209
+
1210
+ // 根据 ID 或索引查找映射
1211
+ function findMapping(identifier) {
1212
+ let index = config.mappings.findIndex(m => m.id === identifier);
1213
+
1214
+ if (index === -1) {
1215
+ const parsed = parseInt(identifier, 10);
1216
+ if (!isNaN(parsed) && parsed >= 0 && parsed < config.mappings.length) {
1217
+ index = parsed;
1218
+ }
1219
+ }
1220
+
1221
+ if (index === -1) {
1222
+ return null;
1223
+ }
1224
+
1225
+ return { mapping: config.mappings[index], index };
1226
+ }
1227
+
1228
+ // 从请求体构造映射对象(保留未提供的字段)
1229
+ function buildMappingFromBody(body, base) {
1230
+ const source = body || {};
1231
+ const listen = source.listen && typeof source.listen === 'object' ? source.listen : {};
1232
+ const target = source.target && typeof source.target === 'object' ? source.target : {};
1233
+
1234
+ return {
1235
+ id: (base && base.id) || generateMappingId(),
1236
+ name: source.name !== undefined ? String(source.name).trim() : (base ? base.name : ''),
1237
+ listen: {
1238
+ host: listen.host !== undefined ? String(listen.host).trim() : (base ? base.listen.host : '0.0.0.0'),
1239
+ port: listen.port !== undefined ? Number(listen.port) : (base ? base.listen.port : 0)
1240
+ },
1241
+ target: {
1242
+ host: target.host !== undefined ? String(target.host).trim() : (base ? base.target.host : ''),
1243
+ port: target.port !== undefined ? Number(target.port) : (base ? base.target.port : 0)
1244
+ }
1245
+ };
1246
+ }
1247
+
1248
+ // ==================== 认证 API ====================
1249
+
578
1250
  // API: 登录
579
1251
  app.post('/api/login', (req, res) => {
580
- const { username, password } = req.body;
581
- const clientIP = req.ip || req.connection.remoteAddress;
582
-
1252
+ if (!authConfig) {
1253
+ return res.status(503).json({
1254
+ success: false,
1255
+ message: '系统尚未配置管理员密码,请先在服务器上运行 tcpmapping setup-password'
1256
+ });
1257
+ }
1258
+
1259
+ const body = req.body || {};
1260
+ const username = typeof body.username === 'string' ? body.username.trim() : '';
1261
+ const password = typeof body.password === 'string' ? body.password : '';
1262
+
1263
+ if (!username || !password) {
1264
+ return res.status(400).json({ success: false, message: '请输入用户名和密码' });
1265
+ }
1266
+
1267
+ const clientIP = req.ip || req.socket.remoteAddress;
1268
+
583
1269
  // 检查登录尝试限制
584
1270
  const attemptCheck = checkLoginAttempts(clientIP);
585
1271
  if (!attemptCheck.allowed) {
586
1272
  return res.status(429).json({ success: false, message: attemptCheck.message });
587
1273
  }
588
-
589
- // 验证用户凭据
1274
+
590
1275
  const validation = validateUserCredentials(username, password);
591
-
592
- if (validation.success) {
593
- // 登录成功
594
- req.session.authenticated = true;
595
- req.session.username = username;
596
- req.session.userRole = validation.user.role;
597
-
598
- // 清除登录失败记录
599
- clearLoginFailures(clientIP);
600
-
601
- log('info', `用户 ${username} 从 ${clientIP} 登录成功`);
602
- res.json({ success: true, message: '登录成功' });
603
- } else {
604
- // 登录失败
1276
+
1277
+ if (!validation.success) {
605
1278
  recordLoginFailure(clientIP);
606
-
607
1279
  log('warn', `用户 ${username} 从 ${clientIP} 登录失败: ${validation.message}`);
608
- res.status(401).json({ success: false, message: validation.message });
1280
+ return res.status(401).json({ success: false, message: validation.message });
609
1281
  }
1282
+
1283
+ // 登录成功后重建会话,防止会话固定攻击
1284
+ req.session.regenerate((regenerateError) => {
1285
+ if (regenerateError) {
1286
+ log('error', `创建会话失败: ${regenerateError.message}`);
1287
+ return res.status(500).json({ success: false, message: '登录失败,请稍后重试' });
1288
+ }
1289
+
1290
+ req.session.authenticated = true;
1291
+ req.session.username = validation.user.username;
1292
+ req.session.userRole = validation.user.role || 'admin';
1293
+
1294
+ req.session.save((saveError) => {
1295
+ if (saveError) {
1296
+ log('error', `保存会话失败: ${saveError.message}`);
1297
+ return res.status(500).json({ success: false, message: '登录失败,请稍后重试' });
1298
+ }
1299
+
1300
+ clearLoginFailures(clientIP);
1301
+ log('info', `用户 ${validation.user.username} 从 ${clientIP} 登录成功`);
1302
+ res.json({ success: true, message: '登录成功' });
1303
+ });
1304
+ });
610
1305
  });
611
1306
 
612
1307
  // API: 登出
613
1308
  app.post('/api/logout', (req, res) => {
614
- req.session.destroy();
615
- res.json({ success: true, message: '已登出' });
1309
+ req.session.destroy(() => {
1310
+ res.clearCookie(SESSION_NAME, { path: '/' });
1311
+ res.json({ success: true, message: '已登出' });
1312
+ });
616
1313
  });
617
1314
 
618
- // 获取指定端口的连接数量
619
- function getConnectionCountByPort(port) {
620
- let count = 0;
621
- for (const conn of connections.values()) {
622
- if (conn.sourcePort === port) {
623
- count++;
624
- }
625
- }
626
- return count;
627
- }
628
-
629
- // 获取指定端口的连接统计
630
- function getConnectionStatsByPort(port) {
631
- let count = 0;
632
- let bytesReceived = 0;
633
- let bytesSent = 0;
634
- for (const conn of connections.values()) {
635
- if (conn.sourcePort === port) {
636
- count++;
637
- bytesReceived += conn.bytesReceived;
638
- bytesSent += conn.bytesSent;
639
- }
640
- }
641
- return { count, bytesReceived, bytesSent };
642
- }
1315
+ // ==================== 映射管理 API ====================
643
1316
 
644
1317
  // API: 获取端口映射列表
645
1318
  app.get('/api/mappings', requireAuth, (req, res) => {
1319
+ const activeStats = buildActiveStats();
1320
+
646
1321
  const mappings = config.mappings.map(mapping => {
647
- const stats = getConnectionStatsByPort(mapping.listen.port);
1322
+ const active = activeStats.get(mapping.id) || { count: 0, bytesReceived: 0, bytesSent: 0 };
1323
+ const total = getMappingStats(mapping.id);
1324
+ const runtime = servers.get(`${mapping.listen.host}:${mapping.listen.port}`);
1325
+
648
1326
  return {
649
1327
  ...mapping,
650
- status: servers.has(`${mapping.listen.host}:${mapping.listen.port}`) ? 'running' : 'stopped',
651
- connectionCount: stats.count,
652
- bytesReceived: stats.bytesReceived,
653
- bytesSent: stats.bytesSent
1328
+ status: runtime && runtime.mapping === mapping ? 'running' : 'stopped',
1329
+ connectionCount: active.count,
1330
+ bytesReceived: active.bytesReceived,
1331
+ bytesSent: active.bytesSent,
1332
+ totalConnections: total.totalConnections,
1333
+ totalBytesReceived: total.bytesReceived,
1334
+ totalBytesSent: total.bytesSent
654
1335
  };
655
1336
  });
1337
+
656
1338
  res.json({ success: true, data: mappings });
657
1339
  });
658
1340
 
659
1341
  // API: 添加端口映射
660
- app.post('/api/mappings', requireAuth, (req, res) => {
661
- const { name, listen, target } = req.body;
662
-
663
- const mapping = {
664
- name: name || '',
665
- listen: {
666
- host: (listen && listen.host) || '0.0.0.0',
667
- port: listen ? parseInt(listen.port) : 0
668
- },
669
- target: {
670
- host: (target && target.host) || '',
671
- port: target ? parseInt(target.port) : 0
672
- }
673
- };
1342
+ app.post('/api/mappings', requireAuth, async (req, res) => {
1343
+ const mapping = buildMappingFromBody(req.body, null);
674
1344
 
675
- // 输入验证
676
1345
  const validation = validateMappingInput(mapping);
677
1346
  if (!validation.isValid) {
678
1347
  return res.status(400).json({ success: false, message: validation.errors.join('; ') });
679
1348
  }
680
1349
 
681
- // 检查端口是否已被使用
682
1350
  const exists = config.mappings.some(m =>
683
1351
  m.listen.host === mapping.listen.host && m.listen.port === mapping.listen.port
684
1352
  );
685
1353
 
686
1354
  if (exists) {
687
- return res.status(400).json({ success: false, message: '端口映射已存在' });
1355
+ return res.status(400).json({ success: false, message: '该监听地址和端口已存在映射' });
688
1356
  }
689
1357
 
690
- // 添加唯一ID
691
- mapping.id = generateMappingId();
1358
+ // 先尝试启动,成功后再写入配置,避免出现"配置存在但端口未监听"的状态
1359
+ const result = await startMapping(mapping);
1360
+ if (!result.success) {
1361
+ return res.status(400).json(result);
1362
+ }
692
1363
 
693
1364
  config.mappings.push(mapping);
1365
+ getMappingStats(mapping.id);
694
1366
 
695
- if (saveConfig(config)) {
696
- const result = startMapping(mapping);
697
- res.json({ ...result, data: mapping });
698
- } else {
699
- res.status(500).json({ success: false, message: '保存配置失败' });
1367
+ if (!saveConfig(config)) {
1368
+ config.mappings.pop();
1369
+ stopMapping(mapping.listen.host, mapping.listen.port);
1370
+ return res.status(500).json({ success: false, message: '保存配置失败' });
700
1371
  }
701
- });
702
-
703
- // API: 删除端口映射 (支持ID或索引)
704
- app.delete('/api/mappings/:identifier', requireAuth, (req, res) => {
705
- const identifier = req.params.identifier;
706
- let mappingIndex = -1;
707
1372
 
708
- // 先尝试通过ID查找
709
- mappingIndex = config.mappings.findIndex(m => m.id === identifier);
710
-
711
- // 如果没找到,尝试作为索引使用(向后兼容)
712
- if (mappingIndex === -1) {
713
- const index = parseInt(identifier);
714
- if (!isNaN(index) && index >= 0 && index < config.mappings.length) {
715
- mappingIndex = index;
716
- }
717
- }
1373
+ res.json({ ...result, data: mapping });
1374
+ });
718
1375
 
719
- if (mappingIndex === -1) {
1376
+ // API: 更新映射配置
1377
+ app.put('/api/mappings/:identifier', requireAuth, async (req, res) => {
1378
+ const found = findMapping(req.params.identifier);
1379
+ if (!found) {
720
1380
  return res.status(404).json({ success: false, message: '映射不存在' });
721
1381
  }
722
1382
 
723
- const mapping = config.mappings[mappingIndex];
1383
+ const { mapping: oldMapping, index } = found;
1384
+ const newMapping = buildMappingFromBody(req.body, oldMapping);
724
1385
 
725
- // 停止服务器
726
- stopMapping(mapping.listen.host, mapping.listen.port);
1386
+ const validation = validateMappingInput(newMapping);
1387
+ if (!validation.isValid) {
1388
+ return res.status(400).json({ success: false, message: validation.errors.join('; ') });
1389
+ }
727
1390
 
728
- // 从配置中移除
729
- config.mappings.splice(mappingIndex, 1);
1391
+ const listenChanged = oldMapping.listen.host !== newMapping.listen.host ||
1392
+ oldMapping.listen.port !== newMapping.listen.port;
730
1393
 
731
- if (saveConfig(config)) {
732
- res.json({ success: true, message: '端口映射已删除' });
733
- } else {
734
- res.status(500).json({ success: false, message: '保存配置失败' });
735
- }
736
- });
1394
+ // 仅修改名称或目标时无需重启监听,避免断开已有连接;
1395
+ // 新连接会自动使用新的目标地址。
1396
+ if (!listenChanged) {
1397
+ const previousName = oldMapping.name;
1398
+ const previousTarget = { ...oldMapping.target };
737
1399
 
738
- // API: 启动单个映射
739
- app.post('/api/mappings/:identifier/start', requireAuth, (req, res) => {
740
- const identifier = req.params.identifier;
741
- let mapping = config.mappings.find(m => m.id === identifier);
1400
+ oldMapping.name = newMapping.name;
1401
+ oldMapping.target = newMapping.target;
742
1402
 
743
- // 向后兼容:尝试索引
744
- if (!mapping) {
745
- const index = parseInt(identifier);
746
- if (!isNaN(index) && index >= 0 && index < config.mappings.length) {
747
- mapping = config.mappings[index];
1403
+ if (!saveConfig(config)) {
1404
+ oldMapping.name = previousName;
1405
+ oldMapping.target = previousTarget;
1406
+ return res.status(500).json({ success: false, message: '保存配置失败' });
748
1407
  }
749
- }
750
1408
 
751
- if (!mapping) {
752
- return res.status(404).json({ success: false, message: '映射不存在' });
1409
+ return res.json({ success: true, message: '端口映射已更新', data: oldMapping });
753
1410
  }
754
1411
 
755
- const result = startMapping(mapping);
756
- res.json(result);
757
- });
1412
+ const exists = config.mappings.some((m, i) =>
1413
+ i !== index &&
1414
+ m.listen.host === newMapping.listen.host &&
1415
+ m.listen.port === newMapping.listen.port
1416
+ );
1417
+ if (exists) {
1418
+ return res.status(400).json({ success: false, message: '该监听地址和端口已存在映射' });
1419
+ }
758
1420
 
759
- // API: 停止单个映射
760
- app.post('/api/mappings/:identifier/stop', requireAuth, (req, res) => {
761
- const identifier = req.params.identifier;
762
- let mapping = config.mappings.find(m => m.id === identifier);
1421
+ const wasRunning = servers.has(`${oldMapping.listen.host}:${oldMapping.listen.port}`);
763
1422
 
764
- // 向后兼容:尝试索引
765
- if (!mapping) {
766
- const index = parseInt(identifier);
767
- if (!isNaN(index) && index >= 0 && index < config.mappings.length) {
768
- mapping = config.mappings[index];
769
- }
1423
+ if (wasRunning) {
1424
+ stopMapping(oldMapping.listen.host, oldMapping.listen.port);
770
1425
  }
771
1426
 
772
- if (!mapping) {
773
- return res.status(404).json({ success: false, message: '映射不存在' });
1427
+ let startResult = { success: true, message: '端口映射已更新' };
1428
+ if (wasRunning) {
1429
+ startResult = await startMapping(newMapping);
1430
+ if (!startResult.success) {
1431
+ // 启动失败,恢复旧映射
1432
+ await startMapping(oldMapping);
1433
+ return res.status(400).json(startResult);
1434
+ }
774
1435
  }
775
1436
 
776
- const result = stopMapping(mapping.listen.host, mapping.listen.port);
777
- res.json(result);
778
- });
1437
+ config.mappings[index] = newMapping;
779
1438
 
780
- // API: 更新映射配置
781
- app.put('/api/mappings/:identifier', requireAuth, (req, res) => {
782
- const identifier = req.params.identifier;
783
- let mappingIndex = config.mappings.findIndex(m => m.id === identifier);
784
-
785
- // 向后兼容:尝试索引
786
- if (mappingIndex === -1) {
787
- const index = parseInt(identifier);
788
- if (!isNaN(index) && index >= 0 && index < config.mappings.length) {
789
- mappingIndex = index;
1439
+ if (!saveConfig(config)) {
1440
+ // 保存失败,回滚
1441
+ config.mappings[index] = oldMapping;
1442
+ if (wasRunning) {
1443
+ stopMapping(newMapping.listen.host, newMapping.listen.port);
1444
+ await startMapping(oldMapping);
790
1445
  }
1446
+ return res.status(500).json({ success: false, message: '保存配置失败' });
791
1447
  }
792
1448
 
793
- if (mappingIndex === -1) {
1449
+ res.json({ success: true, message: '端口映射已更新', data: newMapping });
1450
+ });
1451
+
1452
+ // API: 删除端口映射 (支持ID或索引)
1453
+ app.delete('/api/mappings/:identifier', requireAuth, (req, res) => {
1454
+ const found = findMapping(req.params.identifier);
1455
+ if (!found) {
794
1456
  return res.status(404).json({ success: false, message: '映射不存在' });
795
1457
  }
796
1458
 
797
- const oldMapping = config.mappings[mappingIndex];
798
- const { name, listen, target } = req.body;
1459
+ const { mapping, index } = found;
1460
+ const wasRunning = servers.has(`${mapping.listen.host}:${mapping.listen.port}`);
799
1461
 
800
- const newMapping = {
801
- id: oldMapping.id || generateMappingId(),
802
- name: name || oldMapping.name,
803
- listen: {
804
- host: (listen && listen.host) || oldMapping.listen.host,
805
- port: listen ? parseInt(listen.port) : oldMapping.listen.port
806
- },
807
- target: {
808
- host: (target && target.host) || oldMapping.target.host,
809
- port: target ? parseInt(target.port) : oldMapping.target.port
1462
+ stopMapping(mapping.listen.host, mapping.listen.port);
1463
+ config.mappings.splice(index, 1);
1464
+ mappingStats.delete(mapping.id);
1465
+
1466
+ if (!saveConfig(config)) {
1467
+ // 保存失败,回滚
1468
+ config.mappings.splice(index, 0, mapping);
1469
+ if (wasRunning) {
1470
+ startMapping(mapping);
810
1471
  }
811
- };
812
-
813
- // 验证新配置
814
- const validation = validateMappingInput(newMapping);
815
- if (!validation.isValid) {
816
- return res.status(400).json({ success: false, message: validation.errors.join('; ') });
1472
+ return res.status(500).json({ success: false, message: '保存配置失败' });
817
1473
  }
818
1474
 
819
- // 检查新端口是否与其他映射冲突(排除自身)
820
- const portChanged = oldMapping.listen.port !== newMapping.listen.port ||
821
- oldMapping.listen.host !== newMapping.listen.host;
822
- if (portChanged) {
823
- const exists = config.mappings.some((m, i) =>
824
- i !== mappingIndex &&
825
- m.listen.host === newMapping.listen.host &&
826
- m.listen.port === newMapping.listen.port
827
- );
828
- if (exists) {
829
- return res.status(400).json({ success: false, message: '端口映射已存在' });
830
- }
831
- }
1475
+ res.json({ success: true, message: '端口映射已删除' });
1476
+ });
832
1477
 
833
- // 停止旧的映射
834
- stopMapping(oldMapping.listen.host, oldMapping.listen.port);
1478
+ // API: 启动单个映射
1479
+ app.post('/api/mappings/:identifier/start', requireAuth, async (req, res) => {
1480
+ const found = findMapping(req.params.identifier);
1481
+ if (!found) {
1482
+ return res.status(404).json({ success: false, message: '映射不存在' });
1483
+ }
835
1484
 
836
- // 更新配置
837
- config.mappings[mappingIndex] = newMapping;
1485
+ const result = await startMapping(found.mapping);
1486
+ res.json(result);
1487
+ });
838
1488
 
839
- if (saveConfig(config)) {
840
- // 启动新的映射
841
- const result = startMapping(newMapping);
842
- res.json({ ...result, data: newMapping });
843
- } else {
844
- res.status(500).json({ success: false, message: '保存配置失败' });
1489
+ // API: 停止单个映射
1490
+ app.post('/api/mappings/:identifier/stop', requireAuth, (req, res) => {
1491
+ const found = findMapping(req.params.identifier);
1492
+ if (!found) {
1493
+ return res.status(404).json({ success: false, message: '映射不存在' });
845
1494
  }
1495
+
1496
+ const result = stopMapping(found.mapping.listen.host, found.mapping.listen.port);
1497
+ res.json(result);
846
1498
  });
847
1499
 
1500
+ // ==================== 监控 API ====================
1501
+
848
1502
  // API: 获取连接状态
849
1503
  app.get('/api/connections', requireAuth, (req, res) => {
850
- const connectionList = Array.from(connections.values());
851
-
852
- // 按端口分组统计
1504
+ const now = Date.now();
1505
+ const connectionList = Array.from(connections.values()).map(conn => ({
1506
+ ...conn,
1507
+ durationMs: Math.max(0, now - conn.startTime.getTime())
1508
+ }));
1509
+
1510
+ // 按监听地址 + 端口分组统计
853
1511
  const stats = {};
854
1512
  connectionList.forEach(conn => {
855
- if (!stats[conn.sourcePort]) {
856
- stats[conn.sourcePort] = {
1513
+ const key = `${conn.sourceHost}:${conn.sourcePort}`;
1514
+ if (!stats[key]) {
1515
+ stats[key] = {
1516
+ host: conn.sourceHost,
857
1517
  port: conn.sourcePort,
858
1518
  mappingName: conn.mappingName,
859
1519
  totalConnections: 0,
860
1520
  connections: []
861
1521
  };
862
1522
  }
863
- stats[conn.sourcePort].totalConnections++;
864
- stats[conn.sourcePort].connections.push(conn);
1523
+ stats[key].totalConnections++;
1524
+ stats[key].connections.push(conn);
865
1525
  });
866
1526
 
867
1527
  res.json({
@@ -873,16 +1533,57 @@ app.get('/api/connections', requireAuth, (req, res) => {
873
1533
  });
874
1534
  });
875
1535
 
1536
+ // API: 获取最近关闭的连接记录(可记录源地址、目的地址、持续时长)
1537
+ app.get('/api/connections/history', requireAuth, (req, res) => {
1538
+ const mappingId = typeof req.query.mappingId === 'string' ? req.query.mappingId : '';
1539
+
1540
+ let limit = parseInt(req.query.limit, 10);
1541
+ if (!Number.isFinite(limit) || limit <= 0) {
1542
+ limit = 50;
1543
+ }
1544
+ limit = Math.min(limit, 200);
1545
+
1546
+ const matched = mappingId
1547
+ ? connectionHistory.filter(item => item.mappingId === mappingId)
1548
+ : connectionHistory;
1549
+
1550
+ res.json({
1551
+ success: true,
1552
+ data: {
1553
+ total: matched.length,
1554
+ limit: limit,
1555
+ items: matched.slice(-limit).reverse().map(item => ({
1556
+ ...item,
1557
+ closeReason: localizeCloseReason(item.closeReason, req.lang)
1558
+ }))
1559
+ }
1560
+ });
1561
+ });
1562
+
876
1563
  // API: 获取系统状态
877
1564
  app.get('/api/status', requireAuth, (req, res) => {
878
1565
  res.json({
879
1566
  success: true,
880
1567
  data: {
1568
+ version: VERSION,
1569
+ username: req.session.username || 'admin',
881
1570
  totalMappings: config.mappings.length,
882
1571
  activeMappings: servers.size,
883
1572
  totalConnections: connections.size,
884
1573
  uptime: process.uptime(),
885
- memoryUsage: process.memoryUsage()
1574
+ memoryUsage: process.memoryUsage(),
1575
+ resources: {
1576
+ cpuPercent: Number(resourceSnapshot.cpuPercent.toFixed(2)),
1577
+ cpuCount: CPU_COUNT,
1578
+ memory: { ...resourceSnapshot.memory },
1579
+ traffic: {
1580
+ receivedPerSecond: Math.round(resourceSnapshot.traffic.receivedPerSecond),
1581
+ sentPerSecond: Math.round(resourceSnapshot.traffic.sentPerSecond),
1582
+ totalReceived: processTraffic.received,
1583
+ totalSent: processTraffic.sent
1584
+ },
1585
+ sampledAt: resourceSnapshot.sampledAt
1586
+ }
886
1587
  }
887
1588
  });
888
1589
  });
@@ -891,6 +1592,7 @@ app.get('/api/status', requireAuth, (req, res) => {
891
1592
  app.get('/api/health', (req, res) => {
892
1593
  res.json({
893
1594
  status: 'ok',
1595
+ version: VERSION,
894
1596
  timestamp: new Date().toISOString(),
895
1597
  uptime: process.uptime(),
896
1598
  activeMappings: servers.size,
@@ -898,52 +1600,105 @@ app.get('/api/health', (req, res) => {
898
1600
  });
899
1601
  });
900
1602
 
1603
+ // 未匹配的 API 返回 JSON 404
1604
+ app.use('/api', (req, res) => {
1605
+ res.status(404).json({ success: false, message: '接口不存在' });
1606
+ });
1607
+
1608
+ // 统一错误处理
1609
+ app.use((err, req, res, next) => {
1610
+ console.error('请求处理错误:', err);
1611
+ if (res.headersSent) {
1612
+ return next(err);
1613
+ }
1614
+ res.status(500).json({ success: false, message: '服务器内部错误' });
1615
+ });
1616
+
901
1617
  // 启动程序
902
- function startServer() {
903
- // 加载配置
1618
+ async function startServer() {
904
1619
  config = loadConfig();
905
-
906
- // 加载认证配置
907
- authConfig = loadAuthConfig();
908
-
909
- // 启动TCP转发服务器
1620
+
910
1621
  console.log('==========================================');
911
- console.log('TCP转发服务器正在启动...');
1622
+ console.log(`TCP转发服务器 v${VERSION} 正在启动...`);
912
1623
  console.log('端口映射配置:');
913
- startAllMappings();
1624
+ await startAllMappings();
914
1625
  console.log('==========================================');
915
-
1626
+
916
1627
  // 启动WebUI服务器
917
- if (config.webui && config.webui.enabled) {
918
- const webPort = config.webui.port || 9999;
919
- const webHost = config.webui.host || '0.0.0.0';
1628
+ if (config.webui && config.webui.enabled !== false) {
1629
+ if (!authConfig) {
1630
+ console.error('WebUI 未启动:缺少可用的认证配置');
1631
+ console.error('请运行 "tcpmapping setup-password" 设置管理员密码后重启服务');
1632
+ return;
1633
+ }
1634
+
1635
+ const webPort = isValidPort(config.webui.port) ? Number(config.webui.port) : 9999;
1636
+ const webHost = isValidHost(config.webui.host) ? config.webui.host : '127.0.0.1';
920
1637
 
921
- app.listen(webPort, webHost, () => {
922
- console.log(`WebUI管理界面已启动: http://${webHost}:${webPort}`);
1638
+ httpServer = app.listen(webPort, webHost, () => {
1639
+ console.log(`WebUI管理界面已启动: http://${webHost === '0.0.0.0' || webHost === '::' ? 'localhost' : webHost}:${webPort}`);
1640
+ if (webHost === '0.0.0.0' || webHost === '::') {
1641
+ console.warn('警告: WebUI 监听在所有网卡上,请确保使用强密码并考虑通过防火墙限制访问');
1642
+ }
923
1643
  console.log('==========================================');
924
1644
  });
1645
+
1646
+ httpServer.on('error', (err) => {
1647
+ if (err.code === 'EADDRINUSE') {
1648
+ console.error(`WebUI 启动失败: 端口 ${webPort} 已被占用`);
1649
+ } else {
1650
+ console.error(`WebUI 启动失败: ${err.message}`);
1651
+ }
1652
+ });
1653
+ } else {
1654
+ console.log('WebUI 已禁用(config.json 中 webui.enabled = false)');
925
1655
  }
926
1656
  }
927
1657
 
928
1658
  // 优雅关闭
929
- function gracefulShutdown() {
930
- log('info', '收到停止信号,正在关闭所有服务器...');
1659
+ function gracefulShutdown(signal) {
1660
+ if (shuttingDown) {
1661
+ return;
1662
+ }
1663
+ shuttingDown = true;
1664
+
1665
+ log('info', `收到 ${signal || '停止'} 信号,正在关闭所有服务器...`);
931
1666
  console.log('正在停止所有服务器...');
932
-
933
- // 停止所有TCP转发服务器
934
- for (const [key, { server }] of servers) {
935
- server.close();
1667
+
1668
+ // 停止接收新连接
1669
+ if (httpServer) {
1670
+ httpServer.close();
1671
+ }
1672
+
1673
+ for (const { server } of servers.values()) {
1674
+ try {
1675
+ server.close();
1676
+ } catch (error) {
1677
+ // 忽略关闭错误
1678
+ }
936
1679
  }
937
-
938
- // 清理连接
939
- connections.clear();
940
-
1680
+ servers.clear();
1681
+
1682
+ // 断开所有转发连接
1683
+ for (const id of Array.from(connectionSockets.keys())) {
1684
+ destroyConnection(id, '服务正在关闭');
1685
+ }
1686
+
941
1687
  console.log('所有服务器已停止');
942
- process.exit(0);
1688
+
1689
+ // 给日志和连接一点清理时间后退出
1690
+ setTimeout(() => process.exit(0), 200).unref();
943
1691
  }
944
1692
 
945
- process.on('SIGINT', gracefulShutdown);
946
- process.on('SIGTERM', gracefulShutdown);
1693
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
1694
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
1695
+
1696
+ process.on('unhandledRejection', (reason) => {
1697
+ console.error('未处理的 Promise 拒绝:', reason);
1698
+ });
947
1699
 
948
1700
  // 启动服务器
949
- startServer();
1701
+ startServer().catch((error) => {
1702
+ console.error('服务器启动失败:', error);
1703
+ process.exit(1);
1704
+ });