@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/setup-password.js
CHANGED
|
@@ -20,32 +20,80 @@ const rl = readline.createInterface({
|
|
|
20
20
|
output: process.stdout
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// 非 TTY 环境(如管道输入)使用行队列,避免缓冲区中的行被丢弃
|
|
24
|
+
const inputQueue = [];
|
|
25
|
+
const inputWaiters = [];
|
|
26
|
+
|
|
27
|
+
function readInputLine(prompt) {
|
|
28
|
+
if (!process.stdin.isTTY) {
|
|
29
|
+
process.stdout.write(prompt);
|
|
30
|
+
|
|
31
|
+
if (inputQueue.length > 0) {
|
|
32
|
+
return Promise.resolve(inputQueue.shift());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
inputWaiters.push(resolve);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
rl.question(prompt, resolve);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!process.stdin.isTTY) {
|
|
46
|
+
rl.on('line', (line) => {
|
|
47
|
+
const waiter = inputWaiters.shift();
|
|
48
|
+
if (waiter) {
|
|
49
|
+
waiter(line);
|
|
50
|
+
} else {
|
|
51
|
+
inputQueue.push(line);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
rl.on('close', () => {
|
|
56
|
+
while (inputWaiters.length > 0) {
|
|
57
|
+
inputWaiters.shift()(null);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
30
61
|
|
|
62
|
+
// 默认密码策略
|
|
63
|
+
const DEFAULT_PASSWORD_POLICY = {
|
|
64
|
+
minLength: 8,
|
|
65
|
+
requireUppercase: true,
|
|
66
|
+
requireLowercase: true,
|
|
67
|
+
requireNumbers: true,
|
|
68
|
+
requireSpecialChars: false,
|
|
69
|
+
passwordExpireDays: 90
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 密码复杂度验证
|
|
73
|
+
function validatePassword(password, policy) {
|
|
74
|
+
const rules = { ...DEFAULT_PASSWORD_POLICY, ...(policy || {}) };
|
|
31
75
|
const errors = [];
|
|
32
|
-
|
|
33
|
-
if (password.length < minLength) {
|
|
34
|
-
errors.push(`密码长度至少需要${minLength}个字符`);
|
|
76
|
+
|
|
77
|
+
if (password.length < rules.minLength) {
|
|
78
|
+
errors.push(`密码长度至少需要${rules.minLength}个字符`);
|
|
35
79
|
}
|
|
36
|
-
|
|
37
|
-
if (
|
|
80
|
+
|
|
81
|
+
if (rules.requireUppercase && !/[A-Z]/.test(password)) {
|
|
38
82
|
errors.push('密码必须包含至少一个大写字母');
|
|
39
83
|
}
|
|
40
|
-
|
|
41
|
-
if (
|
|
84
|
+
|
|
85
|
+
if (rules.requireLowercase && !/[a-z]/.test(password)) {
|
|
42
86
|
errors.push('密码必须包含至少一个小写字母');
|
|
43
87
|
}
|
|
44
|
-
|
|
45
|
-
if (
|
|
88
|
+
|
|
89
|
+
if (rules.requireNumbers && !/\d/.test(password)) {
|
|
46
90
|
errors.push('密码必须包含至少一个数字');
|
|
47
91
|
}
|
|
48
|
-
|
|
92
|
+
|
|
93
|
+
if (rules.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>_\-+=[\]\\/;'`~]/.test(password)) {
|
|
94
|
+
errors.push('密码必须包含至少一个特殊字符');
|
|
95
|
+
}
|
|
96
|
+
|
|
49
97
|
return {
|
|
50
98
|
isValid: errors.length === 0,
|
|
51
99
|
errors: errors
|
|
@@ -55,36 +103,73 @@ function validatePassword(password) {
|
|
|
55
103
|
// 获取用户输入(隐藏密码)
|
|
56
104
|
function getPassword(prompt) {
|
|
57
105
|
return new Promise((resolve) => {
|
|
58
|
-
//
|
|
106
|
+
// 非 TTY 环境(如管道输入)无法使用原始模式,退回到普通输入
|
|
107
|
+
if (!process.stdin.isTTY) {
|
|
108
|
+
readInputLine(prompt).then(resolve);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Windows 兼容:使用简单的输入方式
|
|
113
|
+
// 在某些终端上密码可能可见,但这是最兼容的方式
|
|
114
|
+
if (process.platform === 'win32') {
|
|
115
|
+
// Windows: 尝试隐藏输入
|
|
116
|
+
const spawn = require('child_process').spawnSync;
|
|
117
|
+
process.stdout.write(prompt);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// 使用 PowerShell 读取密码
|
|
121
|
+
const result = spawn('powershell', [
|
|
122
|
+
'-Command',
|
|
123
|
+
'$pwd = Read-Host -AsSecureString; [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd))'
|
|
124
|
+
], { stdio: ['inherit', 'pipe', 'inherit'] });
|
|
125
|
+
|
|
126
|
+
if (result.status === 0 && result.stdout) {
|
|
127
|
+
resolve(result.stdout.toString().trim());
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// PowerShell 不可用,回退到普通输入
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 回退方案:普通输入(密码可见)
|
|
135
|
+
console.log('(密码将可见)');
|
|
136
|
+
rl.question('', (answer) => resolve(answer));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Unix/Mac: 使用 raw mode 隐藏密码
|
|
59
141
|
process.stdout.write(prompt);
|
|
60
142
|
process.stdin.setRawMode(true);
|
|
61
143
|
process.stdin.setEncoding('utf8');
|
|
62
|
-
|
|
144
|
+
|
|
63
145
|
let password = '';
|
|
64
|
-
|
|
146
|
+
|
|
65
147
|
const onData = (char) => {
|
|
66
148
|
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
67
|
-
// 结束输入
|
|
68
149
|
process.stdin.setRawMode(false);
|
|
69
150
|
process.stdin.removeListener('data', onData);
|
|
151
|
+
process.stdin.pause();
|
|
70
152
|
process.stdout.write('\n');
|
|
71
153
|
resolve(password);
|
|
72
154
|
} else if (char === '\u0003') {
|
|
73
|
-
// Ctrl+C
|
|
74
|
-
process.
|
|
75
|
-
|
|
76
|
-
|
|
155
|
+
// Ctrl+C: 恢复终端状态后退出
|
|
156
|
+
process.stdin.setRawMode(false);
|
|
157
|
+
process.stdin.removeListener('data', onData);
|
|
158
|
+
process.stdin.pause();
|
|
159
|
+
process.stdout.write('\n');
|
|
160
|
+
process.exit(130);
|
|
161
|
+
} else if (char === '\u007f' || char === '\b') {
|
|
77
162
|
if (password.length > 0) {
|
|
78
163
|
password = password.slice(0, -1);
|
|
79
164
|
process.stdout.write('\b \b');
|
|
80
165
|
}
|
|
81
|
-
} else {
|
|
82
|
-
// 普通字符
|
|
166
|
+
} else if (char.charCodeAt(0) >= 32) {
|
|
83
167
|
password += char;
|
|
84
168
|
process.stdout.write('*');
|
|
85
169
|
}
|
|
86
170
|
};
|
|
87
|
-
|
|
171
|
+
|
|
172
|
+
process.stdin.resume();
|
|
88
173
|
process.stdin.on('data', onData);
|
|
89
174
|
});
|
|
90
175
|
}
|
|
@@ -94,19 +179,24 @@ function loadAuthConfig() {
|
|
|
94
179
|
try {
|
|
95
180
|
const configPath = path.join(getConfigDir(), 'auth-config.json');
|
|
96
181
|
const configData = fs.readFileSync(configPath, 'utf8');
|
|
97
|
-
|
|
182
|
+
const config = JSON.parse(configData);
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(config.users)) {
|
|
185
|
+
config.users = [];
|
|
186
|
+
}
|
|
187
|
+
config.passwordPolicy = { ...DEFAULT_PASSWORD_POLICY, ...(config.passwordPolicy || {}) };
|
|
188
|
+
config.sessionConfig = config.sessionConfig || {
|
|
189
|
+
sessionTimeout: 24,
|
|
190
|
+
maxFailedAttempts: 5,
|
|
191
|
+
lockoutDuration: 30
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return config;
|
|
98
195
|
} catch (error) {
|
|
99
196
|
// 如果文件不存在,返回默认配置
|
|
100
197
|
return {
|
|
101
198
|
users: [],
|
|
102
|
-
passwordPolicy: {
|
|
103
|
-
minLength: 8,
|
|
104
|
-
requireUppercase: true,
|
|
105
|
-
requireLowercase: true,
|
|
106
|
-
requireNumbers: true,
|
|
107
|
-
requireSpecialChars: false,
|
|
108
|
-
passwordExpireDays: 90
|
|
109
|
-
},
|
|
199
|
+
passwordPolicy: { ...DEFAULT_PASSWORD_POLICY },
|
|
110
200
|
sessionConfig: {
|
|
111
201
|
sessionTimeout: 24,
|
|
112
202
|
maxFailedAttempts: 5,
|
|
@@ -116,15 +206,26 @@ function loadAuthConfig() {
|
|
|
116
206
|
}
|
|
117
207
|
}
|
|
118
208
|
|
|
119
|
-
//
|
|
209
|
+
// 保存认证配置(原子写入,且仅当前用户可读写)
|
|
120
210
|
function saveAuthConfig(config) {
|
|
121
211
|
const configDir = getConfigDir();
|
|
122
212
|
// 确保目录存在
|
|
123
213
|
if (!fs.existsSync(configDir)) {
|
|
124
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
214
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
125
215
|
}
|
|
216
|
+
|
|
126
217
|
const configPath = path.join(configDir, 'auth-config.json');
|
|
127
|
-
|
|
218
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
221
|
+
fs.renameSync(tmpPath, configPath);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
fs.chmodSync(configPath, 0o600);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Windows 等平台可能不支持 chmod,忽略
|
|
227
|
+
}
|
|
228
|
+
|
|
128
229
|
console.log(`配置已保存到: ${configPath}`);
|
|
129
230
|
}
|
|
130
231
|
|
|
@@ -137,41 +238,48 @@ async function setupPassword() {
|
|
|
137
238
|
const authConfig = loadAuthConfig();
|
|
138
239
|
|
|
139
240
|
// 获取用户名
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
241
|
+
const usernameInput = await readInputLine('请输入用户名 (默认: admin): ');
|
|
242
|
+
const username = (usernameInput || '').trim() || 'admin';
|
|
243
|
+
|
|
146
244
|
let password = '';
|
|
147
245
|
let isValidPassword = false;
|
|
148
|
-
|
|
246
|
+
|
|
149
247
|
// 获取并验证密码
|
|
150
248
|
while (!isValidPassword) {
|
|
151
249
|
password = await getPassword('请输入新密码: ');
|
|
152
|
-
|
|
250
|
+
|
|
251
|
+
if (password === null) {
|
|
252
|
+
console.log('\n输入已结束,密码设置已取消');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
153
256
|
if (password.length === 0) {
|
|
154
257
|
console.log('密码不能为空,请重新输入');
|
|
155
258
|
continue;
|
|
156
259
|
}
|
|
157
|
-
|
|
158
|
-
const validation = validatePassword(password);
|
|
159
|
-
|
|
260
|
+
|
|
261
|
+
const validation = validatePassword(password, authConfig.passwordPolicy);
|
|
262
|
+
|
|
160
263
|
if (!validation.isValid) {
|
|
161
264
|
console.log('\n密码不符合要求:');
|
|
162
265
|
validation.errors.forEach(error => console.log(` - ${error}`));
|
|
163
266
|
console.log('');
|
|
164
267
|
continue;
|
|
165
268
|
}
|
|
166
|
-
|
|
269
|
+
|
|
167
270
|
// 确认密码
|
|
168
271
|
const confirmPassword = await getPassword('请再次输入密码确认: ');
|
|
169
|
-
|
|
272
|
+
|
|
273
|
+
if (confirmPassword === null) {
|
|
274
|
+
console.log('\n输入已结束,密码设置已取消');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
170
278
|
if (password !== confirmPassword) {
|
|
171
279
|
console.log('\n两次输入的密码不一致,请重新输入\n');
|
|
172
280
|
continue;
|
|
173
281
|
}
|
|
174
|
-
|
|
282
|
+
|
|
175
283
|
isValidPassword = true;
|
|
176
284
|
}
|
|
177
285
|
|
|
@@ -195,6 +303,8 @@ async function setupPassword() {
|
|
|
195
303
|
// 更新现有用户
|
|
196
304
|
authConfig.users[userIndex].passwordHash = passwordHash;
|
|
197
305
|
authConfig.users[userIndex].lastPasswordChange = new Date().toISOString();
|
|
306
|
+
// 重置密码时同时启用账户,避免因账户被禁用而无法登录
|
|
307
|
+
authConfig.users[userIndex].enabled = true;
|
|
198
308
|
console.log(`\n✓ 已更新用户密码: ${username}`);
|
|
199
309
|
}
|
|
200
310
|
|
|
@@ -202,12 +312,16 @@ async function setupPassword() {
|
|
|
202
312
|
saveAuthConfig(authConfig);
|
|
203
313
|
|
|
204
314
|
console.log('✓ 密码设置完成');
|
|
315
|
+
const policy = authConfig.passwordPolicy;
|
|
205
316
|
console.log('\n密码策略要求:');
|
|
206
|
-
console.log(
|
|
207
|
-
console.log(' - 包含大写字母');
|
|
208
|
-
console.log(' - 包含小写字母');
|
|
209
|
-
console.log(' - 包含数字');
|
|
210
|
-
console.log(
|
|
317
|
+
console.log(` - 最少${policy.minLength}个字符`);
|
|
318
|
+
if (policy.requireUppercase) console.log(' - 包含大写字母');
|
|
319
|
+
if (policy.requireLowercase) console.log(' - 包含小写字母');
|
|
320
|
+
if (policy.requireNumbers) console.log(' - 包含数字');
|
|
321
|
+
if (policy.requireSpecialChars) console.log(' - 包含特殊字符');
|
|
322
|
+
if (policy.passwordExpireDays > 0) {
|
|
323
|
+
console.log(` - 密码有效期: ${policy.passwordExpireDays}天`);
|
|
324
|
+
}
|
|
211
325
|
|
|
212
326
|
} catch (error) {
|
|
213
327
|
console.error('设置密码时发生错误:', error.message);
|