@chenpu17/tcpmapping 2.1.0 → 2.4.0
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/CHANGELOG.md +92 -0
- package/README.md +78 -8
- package/bin/tcpmapping.js +363 -173
- package/docs/API.md +532 -0
- package/docs/screenshots/dashboard-en-dark.webp +0 -0
- package/docs/screenshots/dashboard-mobile-zh.webp +0 -0
- package/docs/screenshots/dashboard-zh-light.webp +0 -0
- package/docs/screenshots/login-zh-light.webp +0 -0
- package/docs//345/212/237/350/203/275/346/200/273/347/273/223.md +200 -0
- package/docs//345/256/211/345/205/250/351/205/215/347/275/256/350/257/264/346/230/216.md +154 -0
- package/package.json +6 -4
- package/public/common.js +411 -0
- package/public/dashboard.html +1624 -437
- package/public/login.html +430 -107
- package/server.js +1219 -464
- package/setup-password.js +173 -59
package/bin/tcpmapping.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const http = require('http');
|
|
7
8
|
|
|
8
9
|
const APP_NAME = 'tcpmapping';
|
|
9
|
-
const DATA_DIR = path.join(os.homedir(), '.tcpmapping');
|
|
10
|
+
const DATA_DIR = process.env.TCPMAPPING_DATA_DIR || path.join(os.homedir(), '.tcpmapping');
|
|
10
11
|
const PID_FILE = path.join(DATA_DIR, 'tcpmapping.pid');
|
|
11
12
|
const LOG_FILE = path.join(DATA_DIR, 'tcpmapping.log');
|
|
12
13
|
const SERVER_FILE = path.join(__dirname, '..', 'server.js');
|
|
@@ -16,254 +17,423 @@ const SETUP_PASSWORD_FILE = path.join(__dirname, '..', 'setup-password.js');
|
|
|
16
17
|
const args = process.argv.slice(2);
|
|
17
18
|
const command = args[0] || 'help';
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
+
// 颜色输出(非 TTY 或设置了 NO_COLOR 时自动关闭)
|
|
21
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
22
|
+
const colorize = (code) => (text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text);
|
|
23
|
+
|
|
20
24
|
const colors = {
|
|
21
|
-
green: (
|
|
22
|
-
red: (
|
|
23
|
-
yellow: (
|
|
24
|
-
blue: (
|
|
25
|
-
cyan: (
|
|
26
|
-
gray: (
|
|
25
|
+
green: colorize(32),
|
|
26
|
+
red: colorize(31),
|
|
27
|
+
yellow: colorize(33),
|
|
28
|
+
blue: colorize(34),
|
|
29
|
+
cyan: colorize(36),
|
|
30
|
+
gray: colorize(90)
|
|
27
31
|
};
|
|
28
32
|
|
|
33
|
+
function delay(ms) {
|
|
34
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// 确保数据目录存在
|
|
30
38
|
function ensureDataDir() {
|
|
31
39
|
if (!fs.existsSync(DATA_DIR)) {
|
|
32
|
-
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
40
|
+
fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
//
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
44
|
+
// 读取配置文件
|
|
45
|
+
function readConfig() {
|
|
46
|
+
const candidates = [
|
|
47
|
+
path.join(process.cwd(), 'config.json'),
|
|
48
|
+
path.join(DATA_DIR, 'config.json')
|
|
49
|
+
];
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
for (const configPath of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(configPath)) {
|
|
54
|
+
return { path: configPath, data: JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(colors.yellow(`警告: 配置文件解析失败 (${configPath}): ${error.message}`));
|
|
58
|
+
}
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
return null;
|
|
61
|
+
return { path: null, data: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 获取配置文件路径(按优先级)
|
|
65
|
+
function getConfigPath() {
|
|
66
|
+
return readConfig().path;
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
// 获取认证配置文件路径
|
|
54
70
|
function getAuthConfigPath() {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
const candidates = [
|
|
72
|
+
path.join(process.cwd(), 'auth-config.json'),
|
|
73
|
+
path.join(DATA_DIR, 'auth-config.json')
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const authPath of candidates) {
|
|
77
|
+
if (fs.existsSync(authPath)) {
|
|
78
|
+
return authPath;
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 获取 WebUI 访问信息
|
|
86
|
+
function getWebuiInfo() {
|
|
87
|
+
const config = readConfig().data;
|
|
88
|
+
const webui = (config && config.webui) || {};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
enabled: webui.enabled !== false,
|
|
92
|
+
host: webui.host || '127.0.0.1',
|
|
93
|
+
port: Number(webui.port) || 9999
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 将监听地址转换为可访问地址
|
|
98
|
+
function getAccessHost(host) {
|
|
99
|
+
if (host === '0.0.0.0' || host === '::' || !host) {
|
|
100
|
+
return 'localhost';
|
|
101
|
+
}
|
|
102
|
+
if (host.includes(':')) {
|
|
103
|
+
return `[${host}]`;
|
|
63
104
|
}
|
|
105
|
+
return host;
|
|
106
|
+
}
|
|
64
107
|
|
|
65
|
-
|
|
108
|
+
// 检查进程是否存在
|
|
109
|
+
function isAlive(pid) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// 无权限说明进程存在(属于其他用户)
|
|
115
|
+
return error.code === 'EPERM';
|
|
116
|
+
}
|
|
66
117
|
}
|
|
67
118
|
|
|
68
119
|
// 检查进程是否运行
|
|
69
120
|
function isRunning() {
|
|
70
121
|
ensureDataDir();
|
|
122
|
+
|
|
71
123
|
if (!fs.existsSync(PID_FILE)) {
|
|
72
|
-
return
|
|
124
|
+
return null;
|
|
73
125
|
}
|
|
74
126
|
|
|
75
|
-
const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
|
|
127
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
128
|
+
|
|
129
|
+
if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) {
|
|
130
|
+
return pid;
|
|
131
|
+
}
|
|
76
132
|
|
|
133
|
+
// 进程不存在,清理过期的 PID 文件
|
|
77
134
|
try {
|
|
78
|
-
process.kill(parseInt(pid), 0);
|
|
79
|
-
return parseInt(pid);
|
|
80
|
-
} catch (e) {
|
|
81
|
-
// 进程不存在,清理PID文件
|
|
82
135
|
fs.unlinkSync(PID_FILE);
|
|
83
|
-
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// 忽略清理失败
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 读取日志末尾内容
|
|
143
|
+
function readLogTail(lineCount) {
|
|
144
|
+
try {
|
|
145
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stat = fs.statSync(LOG_FILE);
|
|
150
|
+
const maxBytes = 64 * 1024;
|
|
151
|
+
const size = Math.min(stat.size, maxBytes);
|
|
152
|
+
if (size === 0) {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const buffer = Buffer.alloc(size);
|
|
157
|
+
const fd = fs.openSync(LOG_FILE, 'r');
|
|
158
|
+
try {
|
|
159
|
+
fs.readSync(fd, buffer, 0, size, stat.size - size);
|
|
160
|
+
} finally {
|
|
161
|
+
fs.closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return buffer.toString('utf8').split('\n').slice(-lineCount).join('\n');
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return null;
|
|
84
167
|
}
|
|
85
168
|
}
|
|
86
169
|
|
|
87
170
|
// 启动服务
|
|
88
|
-
function start() {
|
|
171
|
+
async function start() {
|
|
89
172
|
const pid = isRunning();
|
|
90
173
|
if (pid) {
|
|
91
|
-
console.log(colors.yellow(`${APP_NAME}
|
|
174
|
+
console.log(colors.yellow(`${APP_NAME} 已在运行 (PID: ${pid})`));
|
|
92
175
|
return;
|
|
93
176
|
}
|
|
94
177
|
|
|
95
|
-
|
|
96
|
-
const configPath = getConfigPath();
|
|
97
|
-
const authPath = getAuthConfigPath();
|
|
98
|
-
|
|
99
|
-
if (!authPath) {
|
|
178
|
+
if (!getAuthConfigPath()) {
|
|
100
179
|
console.log(colors.yellow('未设置管理员密码,请先运行:'));
|
|
101
180
|
console.log(colors.cyan(' tcpmapping setup-password'));
|
|
102
181
|
return;
|
|
103
182
|
}
|
|
104
183
|
|
|
105
184
|
ensureDataDir();
|
|
106
|
-
console.log(colors.blue(
|
|
185
|
+
console.log(colors.blue(`正在启动 ${APP_NAME}...`));
|
|
107
186
|
|
|
108
187
|
const out = fs.openSync(LOG_FILE, 'a');
|
|
109
188
|
const err = fs.openSync(LOG_FILE, 'a');
|
|
110
189
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
190
|
+
let child;
|
|
191
|
+
try {
|
|
192
|
+
child = spawn(process.execPath, [SERVER_FILE], {
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: ['ignore', out, err],
|
|
195
|
+
// 使用当前目录,以便 server 可以找到当前目录的 config.json
|
|
196
|
+
cwd: process.cwd()
|
|
197
|
+
});
|
|
198
|
+
} finally {
|
|
199
|
+
// 子进程已继承文件描述符,父进程可以关闭
|
|
200
|
+
fs.closeSync(out);
|
|
201
|
+
fs.closeSync(err);
|
|
202
|
+
}
|
|
116
203
|
|
|
117
|
-
fs.writeFileSync(PID_FILE, child.pid
|
|
204
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
118
205
|
child.unref();
|
|
119
206
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
207
|
+
// 短暂等待,捕获启动即失败的情况(如端口被占用、配置错误)
|
|
208
|
+
await delay(500);
|
|
209
|
+
|
|
210
|
+
if (!isAlive(child.pid)) {
|
|
211
|
+
try {
|
|
212
|
+
fs.unlinkSync(PID_FILE);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// 忽略
|
|
215
|
+
}
|
|
216
|
+
console.log(colors.red(`${APP_NAME} 启动失败`));
|
|
217
|
+
const tail = readLogTail(15);
|
|
218
|
+
if (tail) {
|
|
219
|
+
console.log(colors.gray('最近日志:'));
|
|
220
|
+
console.log(tail);
|
|
221
|
+
}
|
|
222
|
+
console.log(colors.gray(`完整日志: ${LOG_FILE}`));
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const webui = getWebuiInfo();
|
|
228
|
+
console.log(colors.green(`${APP_NAME} 启动成功 (PID: ${child.pid})`));
|
|
229
|
+
if (webui.enabled) {
|
|
230
|
+
console.log(colors.cyan(`WebUI: http://${getAccessHost(webui.host)}:${webui.port}`));
|
|
231
|
+
if (webui.host === '0.0.0.0' || webui.host === '::') {
|
|
232
|
+
console.log(colors.yellow('提示: WebUI 监听在所有网卡上,请确保使用强密码'));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(colors.gray(`日志文件: ${LOG_FILE}`));
|
|
236
|
+
const configPath = getConfigPath();
|
|
123
237
|
if (configPath) {
|
|
124
|
-
console.log(colors.gray(
|
|
238
|
+
console.log(colors.gray(`配置文件: ${configPath}`));
|
|
125
239
|
}
|
|
126
240
|
}
|
|
127
241
|
|
|
128
242
|
// 停止服务
|
|
129
|
-
function stop() {
|
|
243
|
+
async function stop() {
|
|
130
244
|
const pid = isRunning();
|
|
131
245
|
if (!pid) {
|
|
132
|
-
console.log(colors.yellow(`${APP_NAME}
|
|
246
|
+
console.log(colors.yellow(`${APP_NAME} 未在运行`));
|
|
133
247
|
return;
|
|
134
248
|
}
|
|
135
249
|
|
|
136
|
-
console.log(colors.blue(
|
|
250
|
+
console.log(colors.blue(`正在停止 ${APP_NAME} (PID: ${pid})...`));
|
|
137
251
|
|
|
138
252
|
try {
|
|
139
253
|
process.kill(pid, 'SIGTERM');
|
|
140
254
|
|
|
141
|
-
//
|
|
255
|
+
// 最多等待 5 秒,让服务优雅退出
|
|
142
256
|
let attempts = 0;
|
|
143
|
-
while (attempts <
|
|
257
|
+
while (attempts < 20 && isAlive(pid)) {
|
|
258
|
+
await delay(250);
|
|
259
|
+
attempts++;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 仍未退出则强制终止
|
|
263
|
+
if (isAlive(pid)) {
|
|
264
|
+
console.log(colors.yellow('服务未在预期时间内退出,正在强制终止...'));
|
|
144
265
|
try {
|
|
145
|
-
process.kill(pid,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
attempts++;
|
|
149
|
-
} catch (e) {
|
|
150
|
-
// 进程已退出
|
|
151
|
-
break;
|
|
266
|
+
process.kill(pid, 'SIGKILL');
|
|
267
|
+
} catch (error) {
|
|
268
|
+
// 进程可能已退出
|
|
152
269
|
}
|
|
270
|
+
await delay(200);
|
|
153
271
|
}
|
|
154
272
|
|
|
155
|
-
// 如果进程仍在运行,强制终止
|
|
156
273
|
try {
|
|
157
|
-
process.kill(pid, 0);
|
|
158
|
-
process.kill(pid, 'SIGKILL');
|
|
159
|
-
} catch (e) {
|
|
160
|
-
// 进程已退出
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (fs.existsSync(PID_FILE)) {
|
|
164
274
|
fs.unlinkSync(PID_FILE);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
// 忽略
|
|
165
277
|
}
|
|
166
278
|
|
|
167
|
-
console.log(colors.green(`${APP_NAME}
|
|
168
|
-
} catch (
|
|
169
|
-
console.log(colors.red(
|
|
279
|
+
console.log(colors.green(`${APP_NAME} 已停止`));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.log(colors.red(`停止 ${APP_NAME} 失败: ${error.message}`));
|
|
170
282
|
}
|
|
171
283
|
}
|
|
172
284
|
|
|
173
285
|
// 重启服务
|
|
174
|
-
function restart() {
|
|
175
|
-
console.log(colors.blue(
|
|
176
|
-
stop();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
286
|
+
async function restart() {
|
|
287
|
+
console.log(colors.blue(`正在重启 ${APP_NAME}...`));
|
|
288
|
+
await stop();
|
|
289
|
+
await delay(300);
|
|
290
|
+
await start();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 请求健康检查接口
|
|
294
|
+
function fetchHealth(host, port) {
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
const request = http.get({
|
|
297
|
+
host: host,
|
|
298
|
+
port: port,
|
|
299
|
+
path: '/api/health',
|
|
300
|
+
timeout: 2000
|
|
301
|
+
}, (res) => {
|
|
302
|
+
let data = '';
|
|
303
|
+
res.on('data', chunk => data += chunk);
|
|
304
|
+
res.on('end', () => {
|
|
305
|
+
try {
|
|
306
|
+
resolve(JSON.parse(data));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
resolve(null);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
request.on('error', () => resolve(null));
|
|
314
|
+
request.on('timeout', () => {
|
|
315
|
+
request.destroy();
|
|
316
|
+
resolve(null);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
180
319
|
}
|
|
181
320
|
|
|
182
321
|
// 查看状态
|
|
183
|
-
function status() {
|
|
322
|
+
async function status() {
|
|
184
323
|
const pid = isRunning();
|
|
185
324
|
const configPath = getConfigPath();
|
|
186
325
|
const authPath = getAuthConfigPath();
|
|
326
|
+
const webui = getWebuiInfo();
|
|
187
327
|
|
|
188
328
|
console.log(colors.cyan('=== TcpMapping Status ===\n'));
|
|
189
329
|
|
|
190
|
-
// 服务状态
|
|
191
330
|
if (pid) {
|
|
192
|
-
console.log(colors.green(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
req.on('error', () => showPaths());
|
|
211
|
-
req.on('timeout', () => { req.destroy(); showPaths(); });
|
|
212
|
-
} catch (e) {
|
|
213
|
-
showPaths();
|
|
331
|
+
console.log(colors.green(`服务状态: 运行中 (PID: ${pid})`));
|
|
332
|
+
|
|
333
|
+
if (webui.enabled) {
|
|
334
|
+
const host = webui.host === '0.0.0.0' || webui.host === '::' ? '127.0.0.1' : webui.host;
|
|
335
|
+
const health = await fetchHealth(host, webui.port);
|
|
336
|
+
|
|
337
|
+
if (health) {
|
|
338
|
+
console.log(` WebUI: http://${getAccessHost(webui.host)}:${webui.port}`);
|
|
339
|
+
console.log(` 运行时间: ${Math.floor(health.uptime / 60)} 分钟`);
|
|
340
|
+
console.log(` 活跃映射: ${health.activeMappings}`);
|
|
341
|
+
console.log(` 活跃连接: ${health.activeConnections}`);
|
|
342
|
+
} else {
|
|
343
|
+
console.log(colors.yellow(` 健康检查失败: http://${getAccessHost(webui.host)}:${webui.port}/api/health`));
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
console.log(colors.gray(' WebUI: 已禁用'));
|
|
214
347
|
}
|
|
215
348
|
} else {
|
|
216
|
-
console.log(colors.red('
|
|
217
|
-
showPaths();
|
|
349
|
+
console.log(colors.red('服务状态: 已停止'));
|
|
218
350
|
}
|
|
219
351
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.log(`Log: ${LOG_FILE}`);
|
|
227
|
-
}
|
|
352
|
+
console.log('');
|
|
353
|
+
console.log(colors.cyan('=== Configuration ===\n'));
|
|
354
|
+
console.log(`数据目录: ${DATA_DIR}`);
|
|
355
|
+
console.log(`配置文件: ${configPath || colors.yellow('未找到 (运行: tcpmapping init)')}`);
|
|
356
|
+
console.log(`认证配置: ${authPath || colors.yellow('未找到 (运行: tcpmapping setup-password)')}`);
|
|
357
|
+
console.log(`日志文件: ${LOG_FILE}`);
|
|
228
358
|
}
|
|
229
359
|
|
|
230
360
|
// 查看日志
|
|
231
361
|
function logs(follow = false) {
|
|
232
362
|
ensureDataDir();
|
|
363
|
+
|
|
233
364
|
if (!fs.existsSync(LOG_FILE)) {
|
|
234
|
-
console.log(colors.yellow('
|
|
365
|
+
console.log(colors.yellow('未找到日志文件'));
|
|
235
366
|
return;
|
|
236
367
|
}
|
|
237
368
|
|
|
238
|
-
if (follow) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
});
|
|
245
|
-
} else {
|
|
246
|
-
const content = fs.readFileSync(LOG_FILE, 'utf8');
|
|
247
|
-
const lines = content.split('\n').slice(-50);
|
|
248
|
-
console.log(lines.join('\n'));
|
|
369
|
+
if (!follow) {
|
|
370
|
+
const tail = readLogTail(50);
|
|
371
|
+
if (tail !== null) {
|
|
372
|
+
console.log(tail);
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
249
375
|
}
|
|
376
|
+
|
|
377
|
+
console.log(colors.blue(`正在跟踪 ${LOG_FILE}... (Ctrl+C 退出)`));
|
|
378
|
+
|
|
379
|
+
const tail = readLogTail(20);
|
|
380
|
+
if (tail) {
|
|
381
|
+
console.log(tail);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let offset = fs.statSync(LOG_FILE).size;
|
|
385
|
+
|
|
386
|
+
// 使用轮询方式,兼容 Windows / macOS / Linux
|
|
387
|
+
const watcher = (current) => {
|
|
388
|
+
if (current.size < offset) {
|
|
389
|
+
// 文件被截断(例如日志轮转)
|
|
390
|
+
offset = 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (current.size > offset) {
|
|
394
|
+
try {
|
|
395
|
+
const length = current.size - offset;
|
|
396
|
+
const buffer = Buffer.alloc(length);
|
|
397
|
+
const fd = fs.openSync(LOG_FILE, 'r');
|
|
398
|
+
try {
|
|
399
|
+
fs.readSync(fd, buffer, 0, length, offset);
|
|
400
|
+
} finally {
|
|
401
|
+
fs.closeSync(fd);
|
|
402
|
+
}
|
|
403
|
+
process.stdout.write(buffer);
|
|
404
|
+
offset = current.size;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// 文件可能在读取过程中被轮转,忽略本次读取
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
fs.watchFile(LOG_FILE, { interval: 500 }, watcher);
|
|
412
|
+
|
|
413
|
+
const cleanup = () => {
|
|
414
|
+
fs.unwatchFile(LOG_FILE, watcher);
|
|
415
|
+
process.exit(0);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
process.on('SIGINT', cleanup);
|
|
419
|
+
process.on('SIGTERM', cleanup);
|
|
250
420
|
}
|
|
251
421
|
|
|
252
422
|
// 设置密码
|
|
253
423
|
function setupPassword() {
|
|
254
424
|
ensureDataDir();
|
|
255
|
-
console.log(colors.blue('
|
|
256
|
-
console.log(colors.gray(
|
|
425
|
+
console.log(colors.blue('正在设置管理员密码...'));
|
|
426
|
+
console.log(colors.gray(`配置将保存到: ${DATA_DIR}`));
|
|
257
427
|
console.log('');
|
|
258
428
|
|
|
259
|
-
const child = spawn(
|
|
429
|
+
const child = spawn(process.execPath, [SETUP_PASSWORD_FILE], {
|
|
260
430
|
stdio: 'inherit',
|
|
261
|
-
cwd: DATA_DIR, // 在数据目录运行,使auth-config.json保存到数据目录
|
|
431
|
+
cwd: DATA_DIR, // 在数据目录运行,使 auth-config.json 保存到数据目录
|
|
262
432
|
env: { ...process.env, TCPMAPPING_DATA_DIR: DATA_DIR }
|
|
263
433
|
});
|
|
264
434
|
|
|
265
435
|
child.on('exit', (code) => {
|
|
266
|
-
process.exit(code);
|
|
436
|
+
process.exit(code === null ? 1 : code);
|
|
267
437
|
});
|
|
268
438
|
}
|
|
269
439
|
|
|
@@ -271,39 +441,47 @@ function setupPassword() {
|
|
|
271
441
|
function init() {
|
|
272
442
|
ensureDataDir();
|
|
273
443
|
|
|
274
|
-
// 检查当前目录和数据目录是否已有配置
|
|
275
444
|
const cwdConfig = path.join(process.cwd(), 'config.json');
|
|
276
445
|
const dataConfig = path.join(DATA_DIR, 'config.json');
|
|
277
446
|
|
|
278
447
|
if (fs.existsSync(cwdConfig)) {
|
|
279
|
-
console.log(colors.yellow(
|
|
448
|
+
console.log(colors.yellow('当前目录已存在 config.json'));
|
|
280
449
|
console.log(colors.gray(` ${cwdConfig}`));
|
|
281
450
|
return;
|
|
282
451
|
}
|
|
283
452
|
|
|
284
453
|
if (fs.existsSync(dataConfig)) {
|
|
285
|
-
console.log(colors.yellow(
|
|
454
|
+
console.log(colors.yellow('数据目录已存在 config.json'));
|
|
286
455
|
console.log(colors.gray(` ${dataConfig}`));
|
|
287
456
|
console.log('');
|
|
288
|
-
console.log('
|
|
457
|
+
console.log('如需为当前项目创建独立配置,可复制一份:');
|
|
289
458
|
console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
|
|
290
459
|
return;
|
|
291
460
|
}
|
|
292
461
|
|
|
293
462
|
const defaultConfig = {
|
|
294
463
|
mappings: [],
|
|
295
|
-
webui: {
|
|
296
|
-
|
|
464
|
+
webui: {
|
|
465
|
+
enabled: true,
|
|
466
|
+
// 默认只监听本机;如需远程访问管理面板,请改为 "0.0.0.0" 并配置防火墙
|
|
467
|
+
host: '127.0.0.1',
|
|
468
|
+
port: 9999
|
|
469
|
+
},
|
|
470
|
+
logging: {
|
|
471
|
+
enabled: true,
|
|
472
|
+
level: 'info'
|
|
473
|
+
},
|
|
474
|
+
timeouts: {
|
|
475
|
+
connect: 30000,
|
|
476
|
+
idle: 300000
|
|
477
|
+
}
|
|
297
478
|
};
|
|
298
479
|
|
|
299
|
-
// 保存到数据目录
|
|
300
480
|
fs.writeFileSync(dataConfig, JSON.stringify(defaultConfig, null, 2));
|
|
301
|
-
console.log(colors.green('
|
|
481
|
+
console.log(colors.green('已创建 config.json'));
|
|
302
482
|
console.log(colors.gray(` ${dataConfig}`));
|
|
303
483
|
console.log('');
|
|
304
|
-
console.log('
|
|
305
|
-
console.log('Or copy it to current directory for project-specific config:');
|
|
306
|
-
console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
|
|
484
|
+
console.log('请编辑配置文件添加端口映射,或启动后通过 WebUI 添加。');
|
|
307
485
|
}
|
|
308
486
|
|
|
309
487
|
// 显示版本
|
|
@@ -317,52 +495,64 @@ function help() {
|
|
|
317
495
|
console.log(`
|
|
318
496
|
${colors.cyan('TCP Mapping Server')} - TCP端口转发服务器
|
|
319
497
|
|
|
320
|
-
${colors.yellow('
|
|
498
|
+
${colors.yellow('用法:')}
|
|
321
499
|
tcpmapping <command>
|
|
322
500
|
|
|
323
|
-
${colors.yellow('
|
|
324
|
-
${colors.green('start')}
|
|
325
|
-
${colors.green('stop')}
|
|
326
|
-
${colors.green('restart')}
|
|
327
|
-
${colors.green('status')}
|
|
328
|
-
${colors.green('logs')}
|
|
329
|
-
${colors.green('logs -f')}
|
|
330
|
-
${colors.green('setup-password')}
|
|
331
|
-
${colors.green('init')}
|
|
332
|
-
${colors.green('version')}
|
|
333
|
-
${colors.green('help')}
|
|
334
|
-
|
|
335
|
-
${colors.yellow('
|
|
336
|
-
1. ./config.json
|
|
337
|
-
2. ~/.tcpmapping/
|
|
338
|
-
|
|
339
|
-
${colors.yellow('
|
|
340
|
-
tcpmapping init #
|
|
341
|
-
tcpmapping setup-password #
|
|
342
|
-
tcpmapping start #
|
|
343
|
-
tcpmapping status #
|
|
501
|
+
${colors.yellow('命令:')}
|
|
502
|
+
${colors.green('start')} 后台启动服务
|
|
503
|
+
${colors.green('stop')} 停止服务
|
|
504
|
+
${colors.green('restart')} 重启服务
|
|
505
|
+
${colors.green('status')} 查看服务状态和配置路径
|
|
506
|
+
${colors.green('logs')} 查看最近日志
|
|
507
|
+
${colors.green('logs -f')} 实时跟踪日志
|
|
508
|
+
${colors.green('setup-password')} 设置管理员密码
|
|
509
|
+
${colors.green('init')} 创建默认 config.json
|
|
510
|
+
${colors.green('version')} 显示版本
|
|
511
|
+
${colors.green('help')} 显示帮助
|
|
512
|
+
|
|
513
|
+
${colors.yellow('配置路径(按优先级):')}
|
|
514
|
+
1. ./config.json 当前目录(项目配置)
|
|
515
|
+
2. ~/.tcpmapping/ 用户数据目录(全局配置)
|
|
516
|
+
|
|
517
|
+
${colors.yellow('示例:')}
|
|
518
|
+
tcpmapping init # 创建配置文件
|
|
519
|
+
tcpmapping setup-password # 设置管理员密码
|
|
520
|
+
tcpmapping start # 启动服务
|
|
521
|
+
tcpmapping status # 查看状态和配置路径
|
|
344
522
|
|
|
345
523
|
${colors.yellow('WebUI:')}
|
|
346
|
-
|
|
524
|
+
启动后访问 http://localhost:9999(默认仅本机可访问)
|
|
347
525
|
`);
|
|
348
526
|
}
|
|
349
527
|
|
|
350
528
|
// 执行命令
|
|
351
529
|
switch (command) {
|
|
352
530
|
case 'start':
|
|
353
|
-
start()
|
|
531
|
+
start().catch(error => {
|
|
532
|
+
console.error(colors.red(`启动失败: ${error.message}`));
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
});
|
|
354
535
|
break;
|
|
355
536
|
case 'stop':
|
|
356
|
-
stop()
|
|
537
|
+
stop().catch(error => {
|
|
538
|
+
console.error(colors.red(`停止失败: ${error.message}`));
|
|
539
|
+
process.exitCode = 1;
|
|
540
|
+
});
|
|
357
541
|
break;
|
|
358
542
|
case 'restart':
|
|
359
|
-
restart()
|
|
543
|
+
restart().catch(error => {
|
|
544
|
+
console.error(colors.red(`重启失败: ${error.message}`));
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
});
|
|
360
547
|
break;
|
|
361
548
|
case 'status':
|
|
362
|
-
status()
|
|
549
|
+
status().catch(error => {
|
|
550
|
+
console.error(colors.red(`获取状态失败: ${error.message}`));
|
|
551
|
+
process.exitCode = 1;
|
|
552
|
+
});
|
|
363
553
|
break;
|
|
364
554
|
case 'logs':
|
|
365
|
-
logs(args[1] === '-f');
|
|
555
|
+
logs(args[1] === '-f' || args[1] === '--follow');
|
|
366
556
|
break;
|
|
367
557
|
case 'setup-password':
|
|
368
558
|
case 'passwd':
|