node-red-contrib-fox-admin 1.0.0 → 1.0.2
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/README.md +14 -14
- package/index.js +93 -9
- package/lib/dashboard.html +1 -1
- package/lib/deploy.html +12 -0
- package/lib/firewall.html +27 -4
- package/lib/network.html +69 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Node-RED 运行时插件,提供系统管理控制台功能。
|
|
|
24
24
|
## 使用方法
|
|
25
25
|
|
|
26
26
|
1. 启动 Node-RED
|
|
27
|
-
2. 访问 `http://your-node-red-ip:port/
|
|
27
|
+
2. 访问 `http://your-node-red-ip:port/foxadmin/`
|
|
28
28
|
3. 使用默认密码登录:`foxcontrol@123`
|
|
29
29
|
4. 修改密码:设置环境变量 `FOX_ADMIN_PASSWORD`
|
|
30
30
|
|
|
@@ -36,27 +36,27 @@ Node-RED 运行时插件,提供系统管理控制台功能。
|
|
|
36
36
|
## API 接口
|
|
37
37
|
|
|
38
38
|
### 认证相关
|
|
39
|
-
- `POST /
|
|
40
|
-
- `POST /
|
|
41
|
-
- `GET /
|
|
39
|
+
- `POST /foxcontrol_api/admin/login`:登录
|
|
40
|
+
- `POST /foxcontrol_api/admin/logout`:登出
|
|
41
|
+
- `GET /foxcontrol_api/admin/check-auth`:检查认证状态
|
|
42
42
|
|
|
43
43
|
### 系统管理
|
|
44
|
-
- `GET /
|
|
45
|
-
- `PUT /
|
|
44
|
+
- `GET /foxcontrol_api/admin/system/info`:获取系统信息
|
|
45
|
+
- `PUT /foxcontrol_api/admin/hostname`:修改主机名
|
|
46
46
|
|
|
47
47
|
### 网络管理
|
|
48
|
-
- `GET /
|
|
49
|
-
- `GET /
|
|
50
|
-
- `PUT /
|
|
48
|
+
- `GET /foxcontrol_api/admin/network/interfaces`:获取网络接口
|
|
49
|
+
- `GET /foxcontrol_api/admin/network/connections`:获取网络连接
|
|
50
|
+
- `PUT /foxcontrol_api/admin/network/config`:配置网络
|
|
51
51
|
|
|
52
52
|
### 防火墙管理
|
|
53
|
-
- `GET /
|
|
54
|
-
- `PUT /
|
|
55
|
-
- `PUT /
|
|
56
|
-
- `POST /
|
|
53
|
+
- `GET /foxcontrol_api/admin/firewall/status`:获取防火墙状态
|
|
54
|
+
- `PUT /foxcontrol_api/admin/firewall/enable`:启用防火墙
|
|
55
|
+
- `PUT /foxcontrol_api/admin/firewall/disable`:禁用防火墙
|
|
56
|
+
- `POST /foxcontrol_api/admin/firewall/rule`:添加防火墙规则
|
|
57
57
|
|
|
58
58
|
### 站点部署
|
|
59
|
-
- `POST /
|
|
59
|
+
- `POST /foxcontrol_api/admin/deploy/upload`:上传并部署站点
|
|
60
60
|
|
|
61
61
|
## 注意事项
|
|
62
62
|
|
package/index.js
CHANGED
|
@@ -234,20 +234,68 @@ module.exports = function(RED) {
|
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
async function getSubnetMask(ifaceName, targetIp) {
|
|
238
|
+
try {
|
|
239
|
+
const osType = getSystemType();
|
|
240
|
+
const platform = process.platform;
|
|
241
|
+
let cmd = '';
|
|
242
|
+
|
|
243
|
+
if (osType === 'debian' || osType === 'kylin' || osType === 'openEuler' || osType === 'ubuntu') {
|
|
244
|
+
if (targetIp) {
|
|
245
|
+
cmd = `ip -4 addr show dev ${ifaceName} 2>/dev/null | grep "inet ${targetIp}/" | awk '{print $2}' | cut -d'/' -f2`;
|
|
246
|
+
} else {
|
|
247
|
+
cmd = `ip -4 addr show dev ${ifaceName} 2>/dev/null | grep 'inet ' | awk 'NR==1 {print $2}' | cut -d'/' -f2`;
|
|
248
|
+
}
|
|
249
|
+
} else if (osType === 'unknown') {
|
|
250
|
+
if (platform === 'win32') {
|
|
251
|
+
cmd = `ipconfig | findstr "${ifaceName}"`;
|
|
252
|
+
} else {
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
return '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = await execCommand(cmd);
|
|
260
|
+
if (platform === 'win32') {
|
|
261
|
+
const match = result.match(/Subnet Mask\s+:\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
262
|
+
return match ? match[1] : '';
|
|
263
|
+
}
|
|
264
|
+
const cidr = result.trim();
|
|
265
|
+
if (cidr && /^\d+$/.test(cidr)) {
|
|
266
|
+
return cidrToDotted(cidr);
|
|
267
|
+
}
|
|
268
|
+
return '';
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return '';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function cidrToDotted(cidr) {
|
|
275
|
+
const mask = ~0 << (32 - cidr);
|
|
276
|
+
return [
|
|
277
|
+
(mask >>> 24) & 255,
|
|
278
|
+
(mask >>> 16) & 255,
|
|
279
|
+
(mask >>> 8) & 255,
|
|
280
|
+
mask & 255
|
|
281
|
+
].join('.');
|
|
282
|
+
}
|
|
283
|
+
|
|
237
284
|
app.get('/foxcontrol_api/admin/network/interfaces', async (req, res) => {
|
|
238
285
|
try {
|
|
239
286
|
const networkInterfaces = await si.networkInterfaces();
|
|
240
287
|
const networkStats = await si.networkStats();
|
|
241
288
|
const defaultGateway = await getDefaultGateway();
|
|
242
289
|
|
|
243
|
-
const interfaces = networkInterfaces.map(iface => {
|
|
290
|
+
const interfaces = await Promise.all(networkInterfaces.map(async (iface) => {
|
|
244
291
|
const stats = networkStats.find(s => s.iface === iface.iface) || {};
|
|
292
|
+
const subnet = iface.ip4_subnet || await getSubnetMask(iface.iface, iface.ip4);
|
|
245
293
|
|
|
246
294
|
return {
|
|
247
295
|
iface: iface.iface,
|
|
248
296
|
type: iface.type,
|
|
249
297
|
ip4: iface.ip4,
|
|
250
|
-
ip4_subnet:
|
|
298
|
+
ip4_subnet: subnet,
|
|
251
299
|
ip6: iface.ip6,
|
|
252
300
|
mac: iface.mac,
|
|
253
301
|
internal: iface.internal,
|
|
@@ -262,7 +310,7 @@ module.exports = function(RED) {
|
|
|
262
310
|
rx_sec: stats.rx_sec || 0,
|
|
263
311
|
tx_sec: stats.tx_sec || 0
|
|
264
312
|
};
|
|
265
|
-
});
|
|
313
|
+
}));
|
|
266
314
|
|
|
267
315
|
res.json({ status: 'success', data: interfaces });
|
|
268
316
|
} catch (err) {
|
|
@@ -303,7 +351,14 @@ iface ${iface} inet static
|
|
|
303
351
|
fs.writeFileSync(configPath, config);
|
|
304
352
|
await execCommand('systemctl restart networking');
|
|
305
353
|
} else if (systemType === 'kylin' || systemType === 'openEuler') {
|
|
306
|
-
|
|
354
|
+
try {
|
|
355
|
+
await execCommand('systemctl restart network');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
try {
|
|
358
|
+
await execCommand(`nmcli con modify ${iface} ipv4.addresses ${ip4}/${netmask} ipv4.gateway ${gateway} ipv4.dns ${dns} ipv4.method manual`);
|
|
359
|
+
await execCommand(`nmcli con up ${iface}`);
|
|
360
|
+
} catch (nmcliErr) {
|
|
361
|
+
const config = `
|
|
307
362
|
TYPE=Ethernet
|
|
308
363
|
BOOTPROTO=static
|
|
309
364
|
DEFROUTE=yes
|
|
@@ -320,9 +375,12 @@ NETMASK=${netmask}
|
|
|
320
375
|
GATEWAY=${gateway}
|
|
321
376
|
DNS1=${dns}
|
|
322
377
|
`;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
378
|
+
const configPath = `/etc/sysconfig/network-scripts/ifcfg-${iface}`;
|
|
379
|
+
fs.writeFileSync(configPath, config);
|
|
380
|
+
await execCommand('nmcli con reload');
|
|
381
|
+
await execCommand(`nmcli con up ${iface}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
326
384
|
}
|
|
327
385
|
|
|
328
386
|
RED.log.info(`[Fox Admin] 网络配置已更新: ${iface}`);
|
|
@@ -343,7 +401,18 @@ DNS1=${dns}
|
|
|
343
401
|
const ufwStatus = await execCommand('ufw status verbose');
|
|
344
402
|
status.enabled = ufwStatus.includes('Status: active');
|
|
345
403
|
const rules = ufwStatus.split('\n').filter(line => line.includes('ALLOW') || line.includes('DENY'));
|
|
346
|
-
status.rules = rules
|
|
404
|
+
status.rules = rules.map(rule => {
|
|
405
|
+
const match = rule.match(/(\d+)\/(tcp|udp)/);
|
|
406
|
+
if (match) {
|
|
407
|
+
return {
|
|
408
|
+
action: rule.includes('ALLOW') ? 'allow' : 'deny',
|
|
409
|
+
port: match[1],
|
|
410
|
+
protocol: match[2],
|
|
411
|
+
ip: rule.includes('from') ? rule.match(/from\s+(\d+\.\d+\.\d+\.\d+)/)?.[1] : null
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return { action: rule.includes('ALLOW') ? 'allow' : 'deny', raw: rule };
|
|
415
|
+
});
|
|
347
416
|
} catch (err) {
|
|
348
417
|
status.enabled = false;
|
|
349
418
|
}
|
|
@@ -352,7 +421,22 @@ DNS1=${dns}
|
|
|
352
421
|
const fwStatus = await execCommand('firewall-cmd --state');
|
|
353
422
|
status.enabled = fwStatus === 'running';
|
|
354
423
|
const rules = await execCommand('firewall-cmd --list-all');
|
|
355
|
-
|
|
424
|
+
|
|
425
|
+
const portsLine = rules.split('\n').find(line => line.trim().startsWith('ports:'));
|
|
426
|
+
if (portsLine) {
|
|
427
|
+
const ports = portsLine.replace('ports:', '').trim();
|
|
428
|
+
if (ports) {
|
|
429
|
+
status.rules = ports.split(/\s+/).map(port => {
|
|
430
|
+
const [portNum, protocol] = port.split('/');
|
|
431
|
+
return {
|
|
432
|
+
action: 'allow',
|
|
433
|
+
port: portNum,
|
|
434
|
+
protocol: protocol || 'tcp',
|
|
435
|
+
ip: null
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
356
440
|
} catch (err) {
|
|
357
441
|
status.enabled = false;
|
|
358
442
|
}
|
package/lib/dashboard.html
CHANGED
package/lib/deploy.html
CHANGED
|
@@ -344,6 +344,18 @@
|
|
|
344
344
|
window.location.href = '/foxadmin';
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
async function checkAuth() {
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch('/foxcontrol_api/admin/check-auth');
|
|
350
|
+
const data = await res.json();
|
|
351
|
+
if (data.status === 'error' || !data.authenticated) {
|
|
352
|
+
window.location.href = '/foxadmin';
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
console.error('[Fox Admin] 登录状态检查失败:', err);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
checkAuth();
|
|
347
359
|
</script>
|
|
348
360
|
</body>
|
|
349
361
|
</html>
|
package/lib/firewall.html
CHANGED
|
@@ -428,14 +428,19 @@
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
container.innerHTML = firewallStatus.rules.map((rule, index) => {
|
|
431
|
-
const isAllow = rule.action === 'allow'
|
|
431
|
+
const isAllow = rule.action === 'allow';
|
|
432
|
+
const portInfo = rule.port ? `${rule.port}/${rule.protocol || 'tcp'}` : (rule.raw || '未知');
|
|
433
|
+
const ipInfo = rule.ip ? `来自: ${rule.ip}` : '';
|
|
434
|
+
|
|
432
435
|
return `
|
|
433
436
|
<div class="rule-item">
|
|
434
437
|
<div class="rule-info">
|
|
435
438
|
<span class="rule-action ${isAllow ? 'rule-allow' : 'rule-deny'}">
|
|
436
|
-
${isAllow ? '
|
|
439
|
+
${isAllow ? '允许' : '拒绝'}
|
|
440
|
+
</span>
|
|
441
|
+
<span class="rule-detail">
|
|
442
|
+
端口: ${portInfo} ${ipInfo}
|
|
437
443
|
</span>
|
|
438
|
-
<span class="rule-detail">${typeof rule === 'string' ? rule : JSON.stringify(rule)}</span>
|
|
439
444
|
</div>
|
|
440
445
|
</div>
|
|
441
446
|
`;
|
|
@@ -508,6 +513,24 @@
|
|
|
508
513
|
return;
|
|
509
514
|
}
|
|
510
515
|
|
|
516
|
+
const currentPort = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
|
|
517
|
+
const currentProtocol = window.location.protocol === 'https:' ? 'tcp' : 'tcp';
|
|
518
|
+
|
|
519
|
+
if (action === 'deny' && port == currentPort && protocol === currentProtocol) {
|
|
520
|
+
if (!ip) {
|
|
521
|
+
if (!confirm('警告:您正在拒绝当前访问的端口(' + port + '/' + protocol + '),这将导致您无法继续访问管理界面!\n\n确定要继续吗?')) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
const currentIP = window.location.hostname;
|
|
526
|
+
if (isValidIP(currentIP) && currentIP === ip) {
|
|
527
|
+
if (!confirm('警告:您正在拒绝当前 IP(' + ip + ')访问端口(' + port + '/' + protocol + '),这将导致您无法继续访问管理界面!\n\n确定要继续吗?')) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
511
534
|
btn.disabled = true;
|
|
512
535
|
btn.textContent = '添加中...';
|
|
513
536
|
|
|
@@ -560,7 +583,7 @@
|
|
|
560
583
|
async function logout() {
|
|
561
584
|
if (confirm('确定要退出登录吗?')) {
|
|
562
585
|
await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
|
|
563
|
-
window.location.href = '
|
|
586
|
+
window.location.href = '/foxadmin';
|
|
564
587
|
}
|
|
565
588
|
}
|
|
566
589
|
|
package/lib/network.html
CHANGED
|
@@ -407,7 +407,7 @@
|
|
|
407
407
|
const data = await res.json();
|
|
408
408
|
|
|
409
409
|
if (data.status === 'success') {
|
|
410
|
-
interfacesData = data.data.filter(iface => !iface.internal
|
|
410
|
+
interfacesData = data.data.filter(iface => !iface.internal);
|
|
411
411
|
renderInterfaces();
|
|
412
412
|
populateInterfaceSelect();
|
|
413
413
|
|
|
@@ -461,6 +461,18 @@
|
|
|
461
461
|
const select = document.getElementById('interface-select');
|
|
462
462
|
select.innerHTML = '<option value="">选择接口</option>' +
|
|
463
463
|
interfacesData.map(iface => `<option value="${iface.iface}">${iface.iface} (${iface.ip4 || '无 IP'})</option>`).join('');
|
|
464
|
+
|
|
465
|
+
select.addEventListener('change', function() {
|
|
466
|
+
const selectedIface = this.value;
|
|
467
|
+
if (!selectedIface) return;
|
|
468
|
+
|
|
469
|
+
const ifaceData = interfacesData.find(iface => iface.iface === selectedIface);
|
|
470
|
+
if (ifaceData) {
|
|
471
|
+
document.getElementById('ip-address').value = ifaceData.ip4 || '';
|
|
472
|
+
document.getElementById('netmask').value = ifaceData.ip4_subnet || '';
|
|
473
|
+
document.getElementById('gateway').value = ifaceData.gateway || '';
|
|
474
|
+
}
|
|
475
|
+
});
|
|
464
476
|
}
|
|
465
477
|
|
|
466
478
|
function formatSpeed(bytes) {
|
|
@@ -491,26 +503,65 @@
|
|
|
491
503
|
btn.disabled = true;
|
|
492
504
|
btn.textContent = '配置中...';
|
|
493
505
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
headers: { 'Content-Type': 'application/json' },
|
|
498
|
-
body: JSON.stringify({ iface, ip4, netmask, gateway, dns })
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
const data = await res.json();
|
|
506
|
+
const currentHost = window.location.hostname;
|
|
507
|
+
const currentPort = window.location.port || '1880';
|
|
508
|
+
const currentProtocol = window.location.protocol;
|
|
502
509
|
|
|
503
|
-
|
|
510
|
+
fetch('/foxcontrol_api/admin/network/config', {
|
|
511
|
+
method: 'PUT',
|
|
512
|
+
headers: { 'Content-Type': 'application/json' },
|
|
513
|
+
body: JSON.stringify({ iface, ip4, netmask, gateway, dns })
|
|
514
|
+
}).then(() => {
|
|
515
|
+
if (currentHost === ip4) {
|
|
504
516
|
showAlert('网络配置已更新!', 'success');
|
|
505
517
|
setTimeout(loadInterfaces, 2000);
|
|
506
|
-
|
|
507
|
-
|
|
518
|
+
btn.disabled = false;
|
|
519
|
+
btn.textContent = '应用配置';
|
|
508
520
|
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
521
|
+
}).catch(() => {
|
|
522
|
+
if (currentHost === ip4) {
|
|
523
|
+
showAlert('网络配置已更新!', 'success');
|
|
524
|
+
setTimeout(loadInterfaces, 2000);
|
|
525
|
+
btn.disabled = false;
|
|
526
|
+
btn.textContent = '应用配置';
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (currentHost !== ip4) {
|
|
531
|
+
showAlert(`网络配置正在应用,5秒后开始检测新地址...`, 'success');
|
|
532
|
+
|
|
533
|
+
setTimeout(async () => {
|
|
534
|
+
showAlert(`正在检测新地址: http://${ip4}:${currentPort}/foxadmin/`, 'success');
|
|
535
|
+
|
|
536
|
+
let checkCount = 0;
|
|
537
|
+
const maxChecks = 30;
|
|
538
|
+
const checkInterval = 2000;
|
|
539
|
+
|
|
540
|
+
const checkNewIP = setInterval(async () => {
|
|
541
|
+
checkCount++;
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const testRes = await fetch(`${currentProtocol}//${ip4}:${currentPort}/foxcontrol_api/admin/check-auth`, {
|
|
545
|
+
mode: 'no-cors',
|
|
546
|
+
cache: 'no-cache',
|
|
547
|
+
signal: AbortSignal.timeout(3000)
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
clearInterval(checkInterval);
|
|
551
|
+
showAlert(`新地址已可用!正在跳转到: http://${ip4}:${currentPort}/foxadmin/`, 'success');
|
|
552
|
+
|
|
553
|
+
setTimeout(() => {
|
|
554
|
+
window.location.href = `${currentProtocol}//${ip4}:${currentPort}/foxadmin/`;
|
|
555
|
+
}, 2000);
|
|
556
|
+
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (checkCount >= maxChecks) {
|
|
559
|
+
clearInterval(checkInterval);
|
|
560
|
+
showAlert(`检测超时,请手动访问新地址: http://${ip4}:${currentPort}/foxadmin/`, 'success');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}, checkInterval);
|
|
564
|
+
}, 5000);
|
|
514
565
|
}
|
|
515
566
|
}
|
|
516
567
|
|
|
@@ -538,7 +589,7 @@
|
|
|
538
589
|
async function logout() {
|
|
539
590
|
if (confirm('确定要退出登录吗?')) {
|
|
540
591
|
await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
|
|
541
|
-
window.location.href = '
|
|
592
|
+
window.location.href = '/foxadmin';
|
|
542
593
|
}
|
|
543
594
|
}
|
|
544
595
|
|