feat: Fix Client Guardian RPC, redesign Netifyd devices UI (v0.6.0-r26)
- Fix Client Guardian JS files: replace invalid 'require X as Y' syntax with direct RPC declarations (LuCI doesn't support as alias) - Add factory default profile to Client Guardian profiles.json - Redesign Netifyd devices page with modern card-based UI: - Device type detection with emoji icons - Gradient summary cards for stats - Responsive grid layout - Traffic distribution bars - Real-time refresh with pulse animation - Fix Netifyd RPC calls: use correct luci.secubox-netifyd object name - Add WAN access control feature to secubox-admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b610239551
commit
b7fb268f71
@ -288,7 +288,66 @@
|
||||
"Bash(do scp /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/$f root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)",
|
||||
"Bash(for f in clients.js overview.js)",
|
||||
"Bash(for f in htdocs/luci-static/resources/view/client-guardian/settings.js htdocs/luci-static/resources/client-guardian/api.js root/usr/libexec/rpcd/luci.client-guardian root/etc/config/client-guardian)",
|
||||
"Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)"
|
||||
"Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)",
|
||||
"Bash(ssh root@192.168.255.1:*)",
|
||||
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus list | grep secubox; ls -la /usr/libexec/rpcd/luci.secubox* 2>/dev/null; ls -la /www/luci-static/resources/view/secubox/ 2>/dev/null\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /usr/share/luci/menu.d/luci-app-secubox.json 2>/dev/null || echo ''NOT FOUND''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | head -50\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus -v list luci.secubox\")",
|
||||
"Bash($SSH root@192.168.255.1 \"echo ''{\"\"method\"\":\"\"getModules\"\"}'' | /usr/libexec/rpcd/luci.secubox call getModules 2>&1 | head -30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"sed -n ''90,100p'' /usr/sbin/secubox-appstore\")",
|
||||
"Bash(SCP=\"scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\":*)",
|
||||
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/sbin/secubox-appstore root@192.168.255.1:/usr/sbin/secubox-appstore)",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart\")",
|
||||
"Bash(SCP=\"scp -O -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart && echo ''✅ Fixed''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | head -30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"grep -A50 ''getModules'' /usr/libexec/rpcd/luci.secubox | head -60\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && /etc/init.d/rpcd restart && ubus call luci.secubox getModules ''{}'' 2>&1 | head -50\")",
|
||||
"Bash($SSH root@192.168.255.1 \"sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | head -80\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /usr/share/secubox/plugins/catalog/crowdsec-dashboard.json 2>/dev/null | head -30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"grep -E ''^\\(luci-app-|secubox-\\)'' /tmp/secubox-installed-cache 2>/dev/null | head -20\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart\")",
|
||||
"Bash($SSH root@192.168.255.1 \"sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 ''crowdsec-dashboard''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 ''netifyd''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/etc/init.d/netifyd status; ls /etc/rc.d/S*netifyd 2>/dev/null\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-appstore && rm -f /tmp/secubox-installed-cache && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox getModules ''{}'' 2>&1 | grep -A12 -E ''\\(netifyd|crowdsec-dashboard\\)''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /etc/config/secubox 2>/dev/null | head -50\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /etc/config/firewall | grep -A5 ''luci\\\\|secubox'' | head -30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/etc/init.d/rpcd restart && sleep 2 && ubus -v list luci.secubox | grep wan\")",
|
||||
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox root@192.168.255.1:/usr/libexec/rpcd/luci.secubox)",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/libexec/rpcd/luci.secubox && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox get_wan_access ''{}''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/usr/sbin/secubox-wan-access status\")",
|
||||
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep secubox_wan\")",
|
||||
"Bash($SSH root@192.168.255.1 \"nft list ruleset 2>/dev/null | grep -A2 ''secubox\\\\|dport 443'' | head -20\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/etc/init.d/firewall restart && sleep 2 && fw4 print 2>/dev/null | grep -i ''secubox\\\\|443'' | head -10\")",
|
||||
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10\")",
|
||||
"Bash($SSH root@192.168.255.1 \"echo ''connected''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''=zone$|\\\\.name=''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''network=|\\\\.input=|\\\\.output=|\\\\.forward=''\")",
|
||||
"Bash(__NEW_LINE__ $SCP /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/secubox-core/root/usr/sbin/secubox-wan-access root@192.168.255.1:/usr/sbin/secubox-wan-access)",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/sbin/secubox-wan-access && /usr/sbin/secubox-wan-access apply && /etc/init.d/firewall restart && sleep 2 && /usr/sbin/secubox-wan-access status\")",
|
||||
"Bash($SSH root@192.168.255.1 \"\n# Remove old invalid rule\nuci delete firewall.@rule[0] 2>/dev/null\n\n# Run apply which will create new rules with correct src\n/usr/sbin/secubox-wan-access apply\n\n# Show the new rule\nuci show firewall | grep secubox\n\n# Restart firewall\n/etc/init.d/firewall restart\n\")",
|
||||
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=15\")",
|
||||
"Bash(SSH=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"echo connected && uci show firewall | grep secubox\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/etc/init.d/firewall restart && sleep 3 && /usr/sbin/secubox-wan-access status\")",
|
||||
"Bash($SSH root@192.168.255.1 \"fw4 print 2>&1 | grep -i ''secubox\\\\|443'' | head -10\")",
|
||||
"Bash($SSH root@192.168.255.1 \"fw4 print 2>&1 | grep -E ''secubox|tcp dport 443'' | head -10\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /usr/share/rpcd/acl.d/luci-app-secubox-admin.json 2>/dev/null || cat /usr/share/rpcd/acl.d/luci-mod-secubox.json 2>/dev/null | head -30\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ls /usr/share/rpcd/acl.d/ | grep -E ''secubox|admin''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"cat /usr/share/rpcd/acl.d/luci-app-secubox-admin.json\")",
|
||||
"Bash($SSH root@192.168.255.1 'cat > /usr/share/rpcd/acl.d/luci-app-secubox-admin.json << ''''EOF''''\n{\n\t\"\"luci-app-secubox-admin\"\": {\n\t\t\"\"description\"\": \"\"SecuBox Admin Control Center\"\",\n\t\t\"\"read\"\": {\n\t\t\t\"\"ubus\"\": {\n\t\t\t\t\"\"luci.secubox\"\": [\n\t\t\t\t\t\"\"get_appstore_apps\"\",\n\t\t\t\t\t\"\"get_appstore_app\"\",\n\t\t\t\t\t\"\"getModules\"\",\n\t\t\t\t\t\"\"getModuleInfo\"\",\n\t\t\t\t\t\"\"get_dashboard_data\"\",\n\t\t\t\t\t\"\"get_system_health\"\",\n\t\t\t\t\t\"\"get_alerts\"\",\n\t\t\t\t\t\"\"getLogs\"\",\n\t\t\t\t\t\"\"listProfiles\"\",\n\t\t\t\t\t\"\"get_catalog_sources\"\",\n\t\t\t\t\t\"\"check_updates\"\",\n\t\t\t\t\t\"\"get_app_versions\"\",\n\t\t\t\t\t\"\"get_changelog\"\",\n\t\t\t\t\t\"\"get_widget_data\"\",\n\t\t\t\t\t\"\"get_wan_access\"\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"\"uci\"\": [\n\t\t\t\t\"\"secubox\"\",\n\t\t\t\t\"\"secubox-appstore\"\",\n\t\t\t\t\"\"firewall\"\",\n\t\t\t\t\"\"*\"\"\n\t\t\t]\n\t\t},\n\t\t\"\"write\"\": {\n\t\t\t\"\"ubus\"\": {\n\t\t\t\t\"\"luci.secubox\"\": [\n\t\t\t\t\t\"\"install_appstore_app\"\",\n\t\t\t\t\t\"\"remove_appstore_app\"\",\n\t\t\t\t\t\"\"enable_module\"\",\n\t\t\t\t\t\"\"disable_module\"\",\n\t\t\t\t\t\"\"updateModule\"\",\n\t\t\t\t\t\"\"applyProfile\"\",\n\t\t\t\t\t\"\"set_catalog_source\"\",\n\t\t\t\t\t\"\"sync_catalog\"\",\n\t\t\t\t\t\"\"set_wan_access\"\",\n\t\t\t\t\t\"\"apply_wan_access\"\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"\"uci\"\": [\n\t\t\t\t\"\"secubox\"\",\n\t\t\t\t\"\"secubox-appstore\"\",\n\t\t\t\t\"\"firewall\"\",\n\t\t\t\t\"\"*\"\"\n\t\t\t]\n\t\t}\n\t}\n}\nEOF\n/etc/init.d/rpcd restart && echo \"\"ACL updated\"\"')",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox apply_wan_access ''{}'' 2>&1\")",
|
||||
"Bash($SSH root@192.168.255.1 \"/usr/sbin/secubox-wan-access apply 2>&1\")",
|
||||
"Bash($SSH root@192.168.255.1 \"ubus call luci.secubox apply_wan_access ''{}''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"chmod 755 /usr/libexec/rpcd/luci.secubox && /etc/init.d/rpcd restart && sleep 2 && ubus call luci.secubox apply_wan_access ''{}''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"uci show network | grep -E ''=interface|\\\\.proto=''\")",
|
||||
"Bash($SSH root@192.168.255.1 \"uci show firewall | grep -E ''zone.*name|=forwarding''\")",
|
||||
"Bash(for:*)",
|
||||
"Bash(do sed -i \"s/''require secubox-theme\\\\/theme as Theme'';//g\" \"$f\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require uci';
|
||||
|
||||
@ -1,16 +1,72 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require client-guardian.api as api';
|
||||
'require rpc';
|
||||
|
||||
var callGetClients = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'clients',
|
||||
expect: { clients: [] }
|
||||
});
|
||||
|
||||
var callGetZones = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'zones',
|
||||
expect: { zones: [] }
|
||||
});
|
||||
|
||||
var callApproveClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'approve_client',
|
||||
params: ['mac', 'name', 'zone', 'notes']
|
||||
});
|
||||
|
||||
var callUpdateClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'update_client',
|
||||
params: ['section', 'name', 'zone', 'notes', 'daily_quota', 'static_ip']
|
||||
});
|
||||
|
||||
var callBanClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'ban_client',
|
||||
params: ['mac', 'reason']
|
||||
});
|
||||
|
||||
var callQuarantineClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'quarantine_client',
|
||||
params: ['mac']
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
i = Math.min(i, units.length - 1);
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function getDeviceIcon(hostname, mac) {
|
||||
hostname = (hostname || '').toLowerCase();
|
||||
mac = (mac || '').toLowerCase();
|
||||
if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/)) return '📱';
|
||||
if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/)) return '💻';
|
||||
if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/)) return '📷';
|
||||
if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/)) return '📺';
|
||||
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
|
||||
if (hostname.match(/router|switch|ap|access[-_]?point|bridge/)) return '🌐';
|
||||
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
|
||||
return '🔌';
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
callGetClients(),
|
||||
callGetZones()
|
||||
]);
|
||||
},
|
||||
|
||||
@ -85,7 +141,7 @@ return view.extend({
|
||||
if (client.status === 'unknown') statusClass += ' quarantine';
|
||||
if (client.status === 'banned') statusClass += ' banned';
|
||||
|
||||
var deviceIcon = api.getDeviceIcon(client.hostname || client.name, client.mac);
|
||||
var deviceIcon = getDeviceIcon(client.hostname || client.name, client.mac);
|
||||
var zoneClass = (client.zone || 'unknown').replace('lan_', '');
|
||||
var self = this;
|
||||
|
||||
@ -109,8 +165,8 @@ return view.extend({
|
||||
]),
|
||||
E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'),
|
||||
E('div', { 'class': 'cg-client-traffic' }, [
|
||||
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + api.formatBytes(client.rx_bytes || 0)),
|
||||
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + api.formatBytes(client.tx_bytes || 0))
|
||||
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + formatBytes(client.rx_bytes || 0)),
|
||||
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + formatBytes(client.tx_bytes || 0))
|
||||
]),
|
||||
E('div', { 'class': 'cg-client-actions' }, [
|
||||
client.status === 'unknown' ? E('div', {
|
||||
@ -192,7 +248,7 @@ return view.extend({
|
||||
var name = document.getElementById('approve-name').value;
|
||||
var zone = document.getElementById('approve-zone').value;
|
||||
var notes = document.getElementById('approve-notes').value;
|
||||
api.approveClient(mac, name, zone, notes).then(L.bind(function() {
|
||||
callApproveClient(mac, name, zone, notes).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
@ -231,7 +287,7 @@ return view.extend({
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
|
||||
api.updateClient(
|
||||
callUpdateClient(
|
||||
client.section,
|
||||
document.getElementById('edit-name').value,
|
||||
document.getElementById('edit-zone').value,
|
||||
@ -262,7 +318,7 @@ return view.extend({
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-danger', 'click': L.bind(function() {
|
||||
var reason = document.getElementById('ban-reason').value || 'Manual ban';
|
||||
api.banClient(mac, reason).then(L.bind(function() {
|
||||
callBanClient(mac, reason).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
|
||||
this.handleRefresh();
|
||||
@ -274,7 +330,7 @@ return view.extend({
|
||||
|
||||
handleUnban: function(ev) {
|
||||
var mac = ev.currentTarget.dataset.mac;
|
||||
api.quarantineClient(mac).then(L.bind(function() {
|
||||
callQuarantineClient(mac).then(L.bind(function() {
|
||||
ui.addNotification(null, E('p', _('Client unbanned successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
@ -282,8 +338,8 @@ return view.extend({
|
||||
|
||||
handleRefresh: function() {
|
||||
return Promise.all([
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
callGetClients(),
|
||||
callGetZones()
|
||||
]).then(L.bind(function(data) {
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
|
||||
@ -1,18 +1,67 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require client-guardian.api as api';
|
||||
'require rpc';
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'status'
|
||||
});
|
||||
|
||||
var callGetClients = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'clients',
|
||||
expect: { clients: [] }
|
||||
});
|
||||
|
||||
var callGetZones = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'zones',
|
||||
expect: { zones: [] }
|
||||
});
|
||||
|
||||
var callApproveClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'approve_client',
|
||||
params: ['mac', 'name', 'zone', 'notes']
|
||||
});
|
||||
|
||||
var callBanClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'ban_client',
|
||||
params: ['mac', 'reason']
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
i = Math.min(i, units.length - 1);
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function getDeviceIcon(hostname, mac) {
|
||||
hostname = (hostname || '').toLowerCase();
|
||||
mac = (mac || '').toLowerCase();
|
||||
if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/)) return '📱';
|
||||
if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/)) return '💻';
|
||||
if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/)) return '📷';
|
||||
if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/)) return '📺';
|
||||
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
|
||||
if (hostname.match(/router|switch|ap|access[-_]?point|bridge/)) return '🌐';
|
||||
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
|
||||
return '🔌';
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
api.getStatus(),
|
||||
api.getClients(),
|
||||
api.getZones(),
|
||||
callGetStatus(),
|
||||
callGetClients(),
|
||||
callGetZones(),
|
||||
uci.load('client-guardian')
|
||||
]);
|
||||
},
|
||||
@ -117,7 +166,7 @@ return view.extend({
|
||||
if (client.status === 'banned')
|
||||
statusClass += ' banned';
|
||||
|
||||
var deviceIcon = api.getDeviceIcon(client.hostname || client.name, client.mac);
|
||||
var deviceIcon = getDeviceIcon(client.hostname || client.name, client.mac);
|
||||
var zoneClass = (client.zone || 'unknown').replace('lan_', '');
|
||||
|
||||
var item = E('div', { 'class': 'cg-client-item ' + statusClass }, [
|
||||
@ -142,8 +191,8 @@ return view.extend({
|
||||
]),
|
||||
E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'),
|
||||
E('div', { 'class': 'cg-client-traffic' }, [
|
||||
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + api.formatBytes(client.rx_bytes || 0)),
|
||||
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + api.formatBytes(client.tx_bytes || 0))
|
||||
E('div', { 'class': 'cg-client-traffic-value' }, '↓ ' + formatBytes(client.rx_bytes || 0)),
|
||||
E('div', { 'class': 'cg-client-traffic-label' }, '↑ ' + formatBytes(client.tx_bytes || 0))
|
||||
])
|
||||
]);
|
||||
|
||||
@ -197,7 +246,7 @@ return view.extend({
|
||||
'class': 'cg-btn cg-btn-success',
|
||||
'click': L.bind(function() {
|
||||
var zone = document.getElementById('approve-zone').value;
|
||||
api.approveClient(mac, '', zone, '').then(L.bind(function() {
|
||||
callApproveClient(mac, '', zone, '').then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
@ -222,7 +271,7 @@ return view.extend({
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-danger',
|
||||
'click': L.bind(function() {
|
||||
api.banClient(mac, 'Manual ban').then(L.bind(function() {
|
||||
callBanClient(mac, 'Manual ban').then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
|
||||
this.handleRefresh();
|
||||
@ -235,24 +284,18 @@ return view.extend({
|
||||
|
||||
handleRefresh: function() {
|
||||
return Promise.all([
|
||||
api.getStatus(),
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
callGetStatus(),
|
||||
callGetClients(),
|
||||
callGetZones()
|
||||
]).then(L.bind(function(data) {
|
||||
// Update dashboard without full page reload
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
// Show loading indicator
|
||||
var statusBadge = document.querySelector('.cg-status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.classList.add('loading');
|
||||
}
|
||||
|
||||
// Reconstruct data array (status, clients, zones, uci already loaded)
|
||||
var newView = this.render(data);
|
||||
dom.content(container.parentNode, newView);
|
||||
|
||||
// Remove loading indicator
|
||||
if (statusBadge) {
|
||||
statusBadge.classList.remove('loading');
|
||||
}
|
||||
@ -263,7 +306,6 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleLeave: function() {
|
||||
// Stop polling when leaving the view to prevent memory leaks
|
||||
poll.stop();
|
||||
},
|
||||
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require client-guardian.api as api';
|
||||
'require rpc';
|
||||
|
||||
var callGetParental = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'parental'
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return api.getParental();
|
||||
return callGetParental();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
|
||||
@ -1,13 +1,29 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require client-guardian.api as api';
|
||||
'require rpc';
|
||||
|
||||
var callGetZones = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'zones',
|
||||
expect: { zones: [] }
|
||||
});
|
||||
|
||||
var callUpdateZone = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'update_zone',
|
||||
params: ['id', 'name', 'bandwidth_limit', 'content_filter']
|
||||
});
|
||||
|
||||
var callSyncZones = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'sync_zones'
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return api.getZones();
|
||||
return callGetZones();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
@ -51,17 +67,17 @@ return view.extend({
|
||||
var features = [];
|
||||
if (zone.internet_access) features.push({ name: 'Internet', enabled: true });
|
||||
else features.push({ name: 'Internet', enabled: false });
|
||||
|
||||
|
||||
if (zone.local_access) features.push({ name: 'Local', enabled: true });
|
||||
else features.push({ name: 'Local', enabled: false });
|
||||
|
||||
|
||||
if (zone.inter_client) features.push({ name: 'Inter-client', enabled: true });
|
||||
|
||||
|
||||
if (zone.time_restrictions) features.push({ name: 'Horaires', enabled: true });
|
||||
if (zone.content_filter && zone.content_filter !== 'none')
|
||||
if (zone.content_filter && zone.content_filter !== 'none')
|
||||
features.push({ name: 'Filtrage', enabled: true });
|
||||
if (zone.portal_required) features.push({ name: 'Portail', enabled: true });
|
||||
if (zone.bandwidth_limit > 0)
|
||||
if (zone.bandwidth_limit > 0)
|
||||
features.push({ name: zone.bandwidth_limit + ' Mbps', enabled: true });
|
||||
|
||||
return E('div', {
|
||||
@ -162,13 +178,11 @@ return view.extend({
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end; margin-top: 20px' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
|
||||
api.updateZone(
|
||||
callUpdateZone(
|
||||
zone.id,
|
||||
zone.name,
|
||||
parseInt(document.getElementById('zone-bandwidth').value) || 0,
|
||||
document.getElementById('zone-filter').value,
|
||||
zone.time_restrictions ? document.getElementById('zone-start').value : '',
|
||||
zone.time_restrictions ? document.getElementById('zone-end').value : ''
|
||||
document.getElementById('zone-filter').value
|
||||
).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Zone updated successfully')), 'success');
|
||||
@ -184,7 +198,7 @@ return view.extend({
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳</span> Synchronisation...';
|
||||
|
||||
api.syncZones().then(function(result) {
|
||||
callSyncZones().then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', {}, 'Zones firewall synchronisées avec succès'), 'success');
|
||||
btn.innerHTML = '<span>✅</span> Synchronisé';
|
||||
@ -205,7 +219,7 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
return api.getZones().then(L.bind(function(data) {
|
||||
return callGetZones().then(L.bind(function(data) {
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
var newView = this.render(data);
|
||||
|
||||
@ -1,5 +1,62 @@
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"id": "factory_default",
|
||||
"name": "Configuration Usine",
|
||||
"description": "Zones firewall par défaut OpenWrt - Réinitialisation standard",
|
||||
"icon": "🔄",
|
||||
"is_factory_default": true,
|
||||
"zones": [
|
||||
{
|
||||
"id": "lan",
|
||||
"name": "Réseau Local (LAN)",
|
||||
"description": "Tous les appareils du réseau local",
|
||||
"network": "lan",
|
||||
"color": "#22c55e",
|
||||
"icon": "home",
|
||||
"internet_access": true,
|
||||
"local_access": true,
|
||||
"inter_client": true,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "normal"
|
||||
},
|
||||
{
|
||||
"id": "wan",
|
||||
"name": "Internet (WAN)",
|
||||
"description": "Connexion vers Internet - trafic sortant autorisé",
|
||||
"network": "wan",
|
||||
"color": "#ef4444",
|
||||
"icon": "globe",
|
||||
"is_wan": true,
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "guest",
|
||||
"name": "Invités",
|
||||
"description": "Réseau invités isolé (optionnel)",
|
||||
"network": "guest",
|
||||
"color": "#8b5cf6",
|
||||
"icon": "users",
|
||||
"optional": true,
|
||||
"internet_access": true,
|
||||
"local_access": false,
|
||||
"inter_client": false,
|
||||
"bandwidth_limit": 0,
|
||||
"priority": "low"
|
||||
}
|
||||
],
|
||||
"firewall_defaults": {
|
||||
"input": "ACCEPT",
|
||||
"output": "ACCEPT",
|
||||
"forward": "REJECT",
|
||||
"lan_to_wan": "ACCEPT",
|
||||
"wan_to_lan": "REJECT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "family_home",
|
||||
"name": "Maison Familiale",
|
||||
|
||||
@ -6,6 +6,25 @@
|
||||
'require rpc';
|
||||
'require form';
|
||||
|
||||
var callGetWanAccess = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'get_wan_access',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callSetWanAccess = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'set_wan_access',
|
||||
params: ['enabled', 'https_enabled', 'https_port', 'http_enabled', 'http_port', 'ssh_enabled', 'ssh_port'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callApplyWanAccess = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'apply_wan_access',
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callGetConfigFiles = rpc.declare({
|
||||
object: 'file',
|
||||
method: 'list',
|
||||
@ -34,12 +53,14 @@ return view.extend({
|
||||
L.resolveDefault(uci.load('secubox-appstore'), {}),
|
||||
L.resolveDefault(uci.load('network'), {}),
|
||||
L.resolveDefault(uci.load('firewall'), {}),
|
||||
L.resolveDefault(uci.load('dhcp'), {})
|
||||
L.resolveDefault(uci.load('dhcp'), {}),
|
||||
L.resolveDefault(callGetWanAccess(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var wanAccess = data[5] || {};
|
||||
|
||||
var container = E('div', { 'class': 'cyberpunk-mode' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||
@ -55,6 +76,7 @@ return view.extend({
|
||||
E('div', { 'class': 'cyber-dual-console' }, [
|
||||
// Left: Quick Config Sections
|
||||
E('div', { 'class': 'cyber-console-left' }, [
|
||||
this.renderWanAccessPanel(wanAccess),
|
||||
this.renderQuickConfigPanel(),
|
||||
this.renderSystemSubsetsPanel(),
|
||||
this.renderConfigFilesPanel()
|
||||
@ -72,6 +94,171 @@ return view.extend({
|
||||
return container;
|
||||
},
|
||||
|
||||
renderWanAccessPanel: function(wanAccess) {
|
||||
var self = this;
|
||||
var enabled = wanAccess.enabled || false;
|
||||
var services = wanAccess.services || {};
|
||||
|
||||
return E('div', { 'class': 'cyber-panel cyber-scanlines' }, [
|
||||
E('div', { 'class': 'cyber-panel-header' }, [
|
||||
E('div', { 'class': 'cyber-panel-title' }, '🌐 WAN ACCESS'),
|
||||
E('span', {
|
||||
'class': 'cyber-panel-badge ' + (enabled ? 'success' : 'warning'),
|
||||
'id': 'wan-access-status'
|
||||
}, enabled ? 'ENABLED' : 'DISABLED')
|
||||
]),
|
||||
E('div', { 'class': 'cyber-panel-body' }, [
|
||||
// Master toggle
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 12px; background: rgba(0,255,65,0.05); border-left: 3px solid var(--cyber-primary); margin-bottom: 15px;' }, [
|
||||
E('div', {}, [
|
||||
E('div', { 'style': 'font-weight: bold; font-size: 13px;' }, 'Remote Access'),
|
||||
E('div', { 'style': 'font-size: 10px; color: var(--cyber-text-dim);' }, 'Allow access from WAN/Internet')
|
||||
]),
|
||||
E('label', { 'class': 'cyber-switch' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'wan-access-master',
|
||||
'checked': enabled,
|
||||
'change': function(ev) {
|
||||
var masterEnabled = ev.target.checked;
|
||||
document.getElementById('wan-access-status').textContent = masterEnabled ? 'ENABLED' : 'DISABLED';
|
||||
document.getElementById('wan-access-status').className = 'cyber-panel-badge ' + (masterEnabled ? 'success' : 'warning');
|
||||
}
|
||||
}),
|
||||
E('span', { 'class': 'cyber-slider' })
|
||||
])
|
||||
]),
|
||||
|
||||
// Service toggles
|
||||
E('div', { 'style': 'display: flex; flex-direction: column; gap: 10px;' }, [
|
||||
// HTTPS
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(0,255,255,0.05); border-radius: 4px;' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('span', { 'style': 'font-size: 16px;' }, '🔒'),
|
||||
E('div', {}, [
|
||||
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'HTTPS (LuCI)'),
|
||||
E('div', { 'style': 'font-size: 10px; color: var(--cyber-text-dim);' }, 'Secure web interface')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('input', {
|
||||
'type': 'number',
|
||||
'id': 'wan-https-port',
|
||||
'value': (services.https && services.https.port) || 443,
|
||||
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
|
||||
}),
|
||||
E('label', { 'class': 'cyber-switch' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'wan-https-enabled',
|
||||
'checked': services.https && services.https.enabled
|
||||
}),
|
||||
E('span', { 'class': 'cyber-slider' })
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// HTTP
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(255,165,0,0.05); border-radius: 4px;' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('span', { 'style': 'font-size: 16px;' }, '🌐'),
|
||||
E('div', {}, [
|
||||
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'HTTP'),
|
||||
E('div', { 'style': 'font-size: 10px; color: var(--cyber-warning);' }, 'Not recommended')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('input', {
|
||||
'type': 'number',
|
||||
'id': 'wan-http-port',
|
||||
'value': (services.http && services.http.port) || 80,
|
||||
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
|
||||
}),
|
||||
E('label', { 'class': 'cyber-switch' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'wan-http-enabled',
|
||||
'checked': services.http && services.http.enabled
|
||||
}),
|
||||
E('span', { 'class': 'cyber-slider' })
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// SSH
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; padding: 10px; background: rgba(255,0,0,0.05); border-radius: 4px;' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('span', { 'style': 'font-size: 16px;' }, '🖥️'),
|
||||
E('div', {}, [
|
||||
E('div', { 'style': 'font-size: 12px; font-weight: bold;' }, 'SSH'),
|
||||
E('div', { 'style': 'font-size: 10px; color: var(--cyber-danger);' }, 'Use with caution')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||
E('input', {
|
||||
'type': 'number',
|
||||
'id': 'wan-ssh-port',
|
||||
'value': (services.ssh && services.ssh.port) || 22,
|
||||
'style': 'width: 70px; padding: 5px; background: rgba(0,0,0,0.3); border: 1px solid var(--cyber-border); color: var(--cyber-text); text-align: center;'
|
||||
}),
|
||||
E('label', { 'class': 'cyber-switch' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'wan-ssh-enabled',
|
||||
'checked': services.ssh && services.ssh.enabled
|
||||
}),
|
||||
E('span', { 'class': 'cyber-slider' })
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Apply button
|
||||
E('div', { 'style': 'margin-top: 15px;' }, [
|
||||
E('button', {
|
||||
'class': 'cyber-btn primary',
|
||||
'style': 'width: 100%;',
|
||||
'click': function() {
|
||||
var masterEnabled = document.getElementById('wan-access-master').checked;
|
||||
var httpsEnabled = document.getElementById('wan-https-enabled').checked;
|
||||
var httpsPort = parseInt(document.getElementById('wan-https-port').value) || 443;
|
||||
var httpEnabled = document.getElementById('wan-http-enabled').checked;
|
||||
var httpPort = parseInt(document.getElementById('wan-http-port').value) || 80;
|
||||
var sshEnabled = document.getElementById('wan-ssh-enabled').checked;
|
||||
var sshPort = parseInt(document.getElementById('wan-ssh-port').value) || 22;
|
||||
|
||||
ui.showModal(_('Applying'), [
|
||||
E('p', { 'class': 'spinning' }, _('Updating firewall rules...'))
|
||||
]);
|
||||
|
||||
callSetWanAccess(
|
||||
masterEnabled ? 1 : 0,
|
||||
httpsEnabled ? 1 : 0,
|
||||
httpsPort,
|
||||
httpEnabled ? 1 : 0,
|
||||
httpPort,
|
||||
sshEnabled ? 1 : 0,
|
||||
sshPort
|
||||
).then(function() {
|
||||
return callApplyWanAccess();
|
||||
}).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', 'WAN access rules applied successfully'), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', 'Failed to apply rules'), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', 'Error: ' + err), 'error');
|
||||
});
|
||||
}
|
||||
}, '🔄 Apply Firewall Rules')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderQuickConfigPanel: function() {
|
||||
var self = this;
|
||||
|
||||
|
||||
@ -17,12 +17,14 @@
|
||||
"check_updates",
|
||||
"get_app_versions",
|
||||
"get_changelog",
|
||||
"get_widget_data"
|
||||
"get_widget_data",
|
||||
"get_wan_access"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"secubox",
|
||||
"secubox-appstore",
|
||||
"firewall",
|
||||
"*"
|
||||
]
|
||||
},
|
||||
@ -36,12 +38,15 @@
|
||||
"updateModule",
|
||||
"applyProfile",
|
||||
"set_catalog_source",
|
||||
"sync_catalog"
|
||||
"sync_catalog",
|
||||
"set_wan_access",
|
||||
"apply_wan_access"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"secubox",
|
||||
"secubox-appstore",
|
||||
"firewall",
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
||||
@ -3,7 +3,52 @@
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require secubox-netifyd/api as netifydAPI';
|
||||
'require rpc';
|
||||
|
||||
var callGetDevices = rpc.declare({
|
||||
object: 'luci.secubox-netifyd',
|
||||
method: 'get_detected_devices'
|
||||
});
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.secubox-netifyd',
|
||||
method: 'get_service_status'
|
||||
});
|
||||
|
||||
var callGetDashboard = rpc.declare({
|
||||
object: 'luci.secubox-netifyd',
|
||||
method: 'get_dashboard'
|
||||
});
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
i = Math.min(i, units.length - 1);
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (seconds < 60) return seconds + 's';
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
||||
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';
|
||||
return Math.floor(seconds / 86400) + 'd';
|
||||
}
|
||||
|
||||
function getDeviceIcon(hostname, mac) {
|
||||
hostname = (hostname || '').toLowerCase();
|
||||
if (hostname.match(/android|phone|mobile|samsung|xiaomi|huawei|oppo|vivo/)) return '📱';
|
||||
if (hostname.match(/iphone|ipad|apple|macbook|imac/)) return '🍎';
|
||||
if (hostname.match(/pc|laptop|desktop|windows|linux|ubuntu/)) return '💻';
|
||||
if (hostname.match(/camera|cam|dvr|nvr|hikvision|dahua/)) return '📷';
|
||||
if (hostname.match(/tv|roku|chromecast|firestick|appletv|smart-tv/)) return '📺';
|
||||
if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) return '🎮';
|
||||
if (hostname.match(/router|switch|ap|access[-_]?point|mesh/)) return '📡';
|
||||
if (hostname.match(/printer|print|hp-|canon-|epson-/)) return '🖨️';
|
||||
if (hostname.match(/alexa|echo|google[-_]?home|homepod/)) return '🔊';
|
||||
if (hostname.match(/thermostat|nest|hue|bulb|sensor|iot/)) return '🏠';
|
||||
return '🔌';
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
refreshInterval: 5,
|
||||
@ -14,9 +59,9 @@ return view.extend({
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
netifydAPI.getDetectedDevices(),
|
||||
netifydAPI.getServiceStatus(),
|
||||
netifydAPI.getDashboard()
|
||||
callGetDevices(),
|
||||
callGetStatus(),
|
||||
callGetDashboard()
|
||||
]);
|
||||
},
|
||||
|
||||
@ -67,7 +112,7 @@ return view.extend({
|
||||
},
|
||||
|
||||
handleExport: function(ev) {
|
||||
var csvContent = 'IP Address,MAC Address,Flows,Bytes Sent,Bytes Received,Total Traffic,Last Seen\n';
|
||||
var csvContent = 'IP Address,MAC Address,Hostname,Flows,Bytes Sent,Bytes Received,Total Traffic,Last Seen\n';
|
||||
|
||||
this.devicesData.forEach(function(device) {
|
||||
var total = (device.bytes_sent || 0) + (device.bytes_received || 0);
|
||||
@ -77,6 +122,7 @@ return view.extend({
|
||||
csvContent += [
|
||||
'"' + (device.ip || 'N/A') + '"',
|
||||
'"' + (device.mac || 'N/A') + '"',
|
||||
'"' + (device.hostname || device.name || '') + '"',
|
||||
device.flows || 0,
|
||||
device.bytes_sent || 0,
|
||||
device.bytes_received || 0,
|
||||
@ -113,45 +159,46 @@ return view.extend({
|
||||
{
|
||||
title: _('Active Devices'),
|
||||
value: devices.length.toString(),
|
||||
icon: 'network-wired',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
emoji: '📱',
|
||||
color: '#6366f1',
|
||||
bg: 'linear-gradient(135deg, rgba(99,102,241,0.15) 0%, rgba(139,92,246,0.15) 100%)'
|
||||
},
|
||||
{
|
||||
title: _('Total Flows'),
|
||||
value: totalFlows.toLocaleString(),
|
||||
icon: 'stream',
|
||||
gradient: 'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)'
|
||||
emoji: '🔄',
|
||||
color: '#10b981',
|
||||
bg: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(52,211,153,0.15) 100%)'
|
||||
},
|
||||
{
|
||||
title: _('Total Sent'),
|
||||
value: netifydAPI.formatBytes(totalBytesSent),
|
||||
icon: 'upload',
|
||||
gradient: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)'
|
||||
title: _('Upload'),
|
||||
value: formatBytes(totalBytesSent),
|
||||
emoji: '⬆️',
|
||||
color: '#ef4444',
|
||||
bg: 'linear-gradient(135deg, rgba(239,68,68,0.15) 0%, rgba(248,113,113,0.15) 100%)'
|
||||
},
|
||||
{
|
||||
title: _('Total Received'),
|
||||
value: netifydAPI.formatBytes(totalBytesRecv),
|
||||
icon: 'download',
|
||||
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
|
||||
title: _('Download'),
|
||||
value: formatBytes(totalBytesRecv),
|
||||
emoji: '⬇️',
|
||||
color: '#22c55e',
|
||||
bg: 'linear-gradient(135deg, rgba(34,197,94,0.15) 0%, rgba(74,222,128,0.15) 100%)'
|
||||
}
|
||||
];
|
||||
|
||||
return E('div', {
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.5rem'
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px'
|
||||
}, cards.map(function(card) {
|
||||
return E('div', {
|
||||
'style': 'background: ' + card.gradient + '; color: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1)'
|
||||
'style': 'background: ' + card.bg + '; border: 1px solid rgba(0,0,0,0.08); padding: 20px; border-radius: 16px; transition: transform 0.2s, box-shadow 0.2s;'
|
||||
}, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem' }, [
|
||||
E('div', { 'style': 'font-size: 0.9em; opacity: 0.9' }, card.title),
|
||||
E('i', {
|
||||
'class': 'fa fa-' + card.icon,
|
||||
'style': 'font-size: 2em; opacity: 0.3'
|
||||
})
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px' }, [
|
||||
E('span', { 'style': 'font-size: 28px' }, card.emoji),
|
||||
E('span', { 'style': 'font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600' }, card.title)
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 2em; font-weight: bold' }, card.value)
|
||||
E('div', { 'style': 'font-size: 28px; font-weight: 700; color: ' + card.color }, card.value)
|
||||
]);
|
||||
}.bind(this)));
|
||||
}));
|
||||
},
|
||||
|
||||
renderDevicesTable: function() {
|
||||
@ -161,135 +208,90 @@ return view.extend({
|
||||
var devices = this.filterDevices(this.devicesData);
|
||||
var sortedDevices = this.sortDevices(devices, this.sortColumn, this.sortDirection);
|
||||
|
||||
var getSortIcon = function(column) {
|
||||
if (this.sortColumn !== column) {
|
||||
return E('i', { 'class': 'fa fa-sort', 'style': 'opacity: 0.3' });
|
||||
}
|
||||
return E('i', {
|
||||
'class': 'fa fa-sort-' + (this.sortDirection === 'asc' ? 'up' : 'down'),
|
||||
'style': 'color: #3b82f6'
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
dom.content(container, [
|
||||
devices.length > 0 ? E('div', { 'class': 'table', 'style': 'font-size: 0.95em' },
|
||||
[
|
||||
// Header
|
||||
E('div', { 'class': 'tr table-titles' }, [
|
||||
E('div', {
|
||||
'class': 'th left',
|
||||
'style': 'width: 20%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'ip')
|
||||
}, [
|
||||
_('IP Address'),
|
||||
' ',
|
||||
getSortIcon('ip')
|
||||
]),
|
||||
E('div', { 'class': 'th left', 'style': 'width: 20%' }, _('MAC Address')),
|
||||
E('div', {
|
||||
'class': 'th center',
|
||||
'style': 'width: 10%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'flows')
|
||||
}, [
|
||||
_('Flows'),
|
||||
' ',
|
||||
getSortIcon('flows')
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'th right',
|
||||
'style': 'width: 15%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'bytes_sent')
|
||||
}, [
|
||||
_('Sent'),
|
||||
' ',
|
||||
getSortIcon('bytes_sent')
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'th right',
|
||||
'style': 'width: 15%; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleSort', 'bytes_received')
|
||||
}, [
|
||||
_('Received'),
|
||||
' ',
|
||||
getSortIcon('bytes_received')
|
||||
]),
|
||||
E('div', { 'class': 'th', 'style': 'width: 20%' }, _('Traffic Distribution'))
|
||||
])
|
||||
].concat(
|
||||
// Rows
|
||||
sortedDevices.map(function(device, idx) {
|
||||
var lastSeen = device.last_seen || 0;
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var ago = now - lastSeen;
|
||||
var lastSeenStr = 'N/A';
|
||||
|
||||
if (lastSeen > 0) {
|
||||
if (ago < 60) {
|
||||
lastSeenStr = _('Just now');
|
||||
} else {
|
||||
lastSeenStr = netifydAPI.formatDuration(ago) + ' ' + _('ago');
|
||||
}
|
||||
}
|
||||
|
||||
var totalBytes = (device.bytes_sent || 0) + (device.bytes_received || 0);
|
||||
var sentPercent = totalBytes > 0 ? ((device.bytes_sent || 0) / totalBytes * 100) : 50;
|
||||
var recvPercent = 100 - sentPercent;
|
||||
|
||||
return E('div', {
|
||||
'class': 'tr',
|
||||
'style': idx % 2 === 0 ? 'background: #f9fafb' : ''
|
||||
}, [
|
||||
E('div', { 'class': 'td left', 'style': 'width: 20%' }, [
|
||||
E('code', { 'style': 'font-size: 0.9em' }, device.ip || 'Unknown'),
|
||||
E('br'),
|
||||
E('small', { 'class': 'text-muted' }, lastSeenStr)
|
||||
]),
|
||||
E('div', { 'class': 'td left', 'style': 'width: 20%' }, [
|
||||
E('code', { 'style': 'font-size: 0.8em' }, device.mac || 'Unknown')
|
||||
]),
|
||||
E('div', { 'class': 'td center', 'style': 'width: 10%' },
|
||||
(device.flows || 0).toLocaleString()),
|
||||
E('div', { 'class': 'td right', 'style': 'width: 15%' }, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #ef4444; color: white; padding: 0.25rem 0.5rem; border-radius: 4px'
|
||||
}, netifydAPI.formatBytes(device.bytes_sent || 0))
|
||||
]),
|
||||
E('div', { 'class': 'td right', 'style': 'width: 15%' }, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #10b981; color: white; padding: 0.25rem 0.5rem; border-radius: 4px'
|
||||
}, netifydAPI.formatBytes(device.bytes_received || 0))
|
||||
]),
|
||||
E('div', { 'class': 'td', 'style': 'width: 20%' }, [
|
||||
E('div', {
|
||||
'style': 'display: flex; gap: 2px; height: 24px; border-radius: 4px; overflow: hidden'
|
||||
}, [
|
||||
E('div', {
|
||||
'style': 'background: #ef4444; width: ' + sentPercent + '%; transition: width 0.3s',
|
||||
'title': _('Upload: %s').format(sentPercent.toFixed(1) + '%')
|
||||
}),
|
||||
E('div', {
|
||||
'style': 'background: #10b981; width: ' + recvPercent + '%; transition: width 0.3s',
|
||||
'title': _('Download: %s').format(recvPercent.toFixed(1) + '%')
|
||||
})
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 0.75em; color: #6b7280; margin-top: 0.25rem; text-align: center' },
|
||||
_('Total: %s').format(netifydAPI.formatBytes(totalBytes)))
|
||||
])
|
||||
]);
|
||||
}.bind(this))
|
||||
)
|
||||
) : E('div', {
|
||||
'class': 'alert-message info',
|
||||
'style': 'text-align: center; padding: 3rem'
|
||||
if (devices.length === 0) {
|
||||
dom.content(container, E('div', {
|
||||
'style': 'text-align: center; padding: 48px; background: linear-gradient(135deg, rgba(99,102,241,0.05) 0%, rgba(139,92,246,0.05) 100%); border-radius: 16px; border: 2px dashed rgba(99,102,241,0.2)'
|
||||
}, [
|
||||
E('i', { 'class': 'fa fa-network-wired', 'style': 'font-size: 3em; opacity: 0.3; display: block; margin-bottom: 1rem' }),
|
||||
E('h4', _('No Device Data')),
|
||||
E('p', { 'class': 'text-muted' }, _('No devices have been detected yet')),
|
||||
E('small', _('Data will appear once network traffic is analyzed'))
|
||||
])
|
||||
]);
|
||||
E('div', { 'style': 'font-size: 48px; margin-bottom: 16px; opacity: 0.5' }, '📡'),
|
||||
E('h4', { 'style': 'margin: 0 0 8px 0; color: #374151' }, _('No Devices Detected')),
|
||||
E('p', { 'style': 'color: #6b7280; margin: 0' }, _('Waiting for network traffic...'))
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
var deviceCards = sortedDevices.map(function(device) {
|
||||
var lastSeen = device.last_seen || 0;
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var ago = now - lastSeen;
|
||||
var lastSeenStr = 'N/A';
|
||||
var isOnline = ago < 120;
|
||||
|
||||
if (lastSeen > 0) {
|
||||
if (ago < 60) lastSeenStr = _('Just now');
|
||||
else lastSeenStr = formatDuration(ago) + ' ' + _('ago');
|
||||
}
|
||||
|
||||
var totalBytes = (device.bytes_sent || 0) + (device.bytes_received || 0);
|
||||
var sentPercent = totalBytes > 0 ? ((device.bytes_sent || 0) / totalBytes * 100) : 50;
|
||||
var recvPercent = 100 - sentPercent;
|
||||
var icon = getDeviceIcon(device.hostname || device.name, device.mac);
|
||||
|
||||
return E('div', {
|
||||
'style': 'background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; transition: all 0.2s; ' + (isOnline ? 'box-shadow: 0 2px 8px rgba(34,197,94,0.1); border-left: 4px solid #22c55e;' : 'opacity: 0.7;')
|
||||
}, [
|
||||
// Header row: Icon + IP/MAC + Status
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 12px' }, [
|
||||
E('div', { 'style': 'font-size: 32px; flex-shrink: 0' }, icon),
|
||||
E('div', { 'style': 'flex: 1; min-width: 0' }, [
|
||||
E('div', { 'style': 'font-weight: 600; color: #111827; font-size: 15px; font-family: monospace' }, device.ip || 'Unknown'),
|
||||
E('div', { 'style': 'font-size: 11px; color: #9ca3af; font-family: monospace' }, device.mac || 'Unknown')
|
||||
]),
|
||||
E('div', { 'style': 'text-align: right' }, [
|
||||
E('div', {
|
||||
'style': 'display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; ' + (isOnline ? 'background: rgba(34,197,94,0.15); color: #16a34a' : 'background: rgba(156,163,175,0.15); color: #6b7280')
|
||||
}, [
|
||||
E('span', { 'style': 'width: 6px; height: 6px; border-radius: 50%; background: ' + (isOnline ? '#22c55e' : '#9ca3af') }),
|
||||
isOnline ? _('Online') : lastSeenStr
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 11px; color: #9ca3af; margin-top: 4px' },
|
||||
(device.flows || 0) + ' ' + _('flows'))
|
||||
])
|
||||
]),
|
||||
// Traffic stats row
|
||||
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px' }, [
|
||||
E('div', { 'style': 'background: rgba(239,68,68,0.08); padding: 8px 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center' }, [
|
||||
E('span', { 'style': 'font-size: 12px; color: #6b7280' }, '⬆️ ' + _('Upload')),
|
||||
E('span', { 'style': 'font-weight: 600; color: #dc2626; font-size: 13px' }, formatBytes(device.bytes_sent || 0))
|
||||
]),
|
||||
E('div', { 'style': 'background: rgba(34,197,94,0.08); padding: 8px 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center' }, [
|
||||
E('span', { 'style': 'font-size: 12px; color: #6b7280' }, '⬇️ ' + _('Download')),
|
||||
E('span', { 'style': 'font-weight: 600; color: #16a34a; font-size: 13px' }, formatBytes(device.bytes_received || 0))
|
||||
])
|
||||
]),
|
||||
// Traffic bar
|
||||
E('div', { 'style': 'position: relative' }, [
|
||||
E('div', { 'style': 'display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: #f3f4f6' }, [
|
||||
E('div', {
|
||||
'style': 'background: linear-gradient(90deg, #ef4444, #f87171); width: ' + sentPercent + '%; transition: width 0.3s',
|
||||
'title': _('Upload: %s').format(sentPercent.toFixed(1) + '%')
|
||||
}),
|
||||
E('div', {
|
||||
'style': 'background: linear-gradient(90deg, #22c55e, #4ade80); width: ' + recvPercent + '%; transition: width 0.3s',
|
||||
'title': _('Download: %s').format(recvPercent.toFixed(1) + '%')
|
||||
})
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #9ca3af' }, [
|
||||
E('span', {}, sentPercent.toFixed(0) + '% ↑'),
|
||||
E('span', { 'style': 'font-weight: 500; color: #6b7280' }, _('Total: %s').format(formatBytes(totalBytes))),
|
||||
E('span', {}, recvPercent.toFixed(0) + '% ↓')
|
||||
])
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
dom.content(container, E('div', {
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px'
|
||||
}, deviceCards));
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
@ -302,101 +304,96 @@ return view.extend({
|
||||
// Set up polling
|
||||
poll.add(L.bind(function() {
|
||||
return Promise.all([
|
||||
netifydAPI.getDetectedDevices(),
|
||||
netifydAPI.getServiceStatus()
|
||||
callGetDevices(),
|
||||
callGetStatus()
|
||||
]).then(L.bind(function(result) {
|
||||
this.devicesData = (result[0] || {}).devices || [];
|
||||
this.renderDevicesTable();
|
||||
// Update summary cards
|
||||
var summaryContainer = document.getElementById('summary-cards-container');
|
||||
if (summaryContainer) {
|
||||
dom.content(summaryContainer, this.renderSummaryCards(this.devicesData).childNodes);
|
||||
}
|
||||
// Update device count badge
|
||||
var countBadge = document.getElementById('device-count-badge');
|
||||
if (countBadge) {
|
||||
countBadge.textContent = this.devicesData.length;
|
||||
}
|
||||
}, this));
|
||||
}, this), this.refreshInterval);
|
||||
|
||||
var serviceRunning = status.running;
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
|
||||
E('h2', { 'name': 'content', 'style': 'margin: 0' }, [
|
||||
E('i', { 'class': 'fa fa-network-wired', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Detected Devices')
|
||||
]),
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'padding: 0.5rem 1rem; font-size: 0.9em; background: ' + (serviceRunning ? '#10b981' : '#ef4444')
|
||||
}, [
|
||||
E('i', { 'class': 'fa fa-circle', 'style': 'margin-right: 0.5rem' }),
|
||||
serviceRunning ? _('Live') : _('Offline')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-map-descr' },
|
||||
_('Network devices detected and tracked by Netifyd deep packet inspection. Updates every 5 seconds.')),
|
||||
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
|
||||
E('h3', { 'style': 'margin: 0' }, [
|
||||
E('i', { 'class': 'fa fa-chart-bar', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Summary')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'btn btn-primary',
|
||||
'click': ui.createHandlerFn(this, 'handleExport')
|
||||
}, [
|
||||
E('i', { 'class': 'fa fa-download' }),
|
||||
' ',
|
||||
_('Export CSV')
|
||||
])
|
||||
]),
|
||||
this.renderSummaryCards(this.devicesData)
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem' }, [
|
||||
E('h3', { 'style': 'margin: 0' }, [
|
||||
E('i', { 'class': 'fa fa-list', 'style': 'margin-right: 0.5rem' }),
|
||||
_('Device List'),
|
||||
' ',
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #3b82f6; color: white; margin-left: 0.5rem'
|
||||
}, this.devicesData.length)
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; gap: 0.5rem; align-items: center' }, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'placeholder': _('Search by IP or MAC...'),
|
||||
'style': 'min-width: 250px',
|
||||
'value': this.searchQuery,
|
||||
'keyup': function(ev) {
|
||||
self.searchQuery = ev.target.value;
|
||||
self.renderDevicesTable();
|
||||
}
|
||||
})
|
||||
return E('div', { 'style': 'max-width: 1400px; margin: 0 auto; padding: 24px' }, [
|
||||
// Header
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
|
||||
E('div', { 'style': 'font-size: 36px' }, '📡'),
|
||||
E('div', {}, [
|
||||
E('h2', { 'style': 'margin: 0; font-size: 24px; font-weight: 700; color: #111827' }, _('Network Devices')),
|
||||
E('p', { 'style': 'margin: 4px 0 0 0; font-size: 14px; color: #6b7280' }, _('Real-time traffic monitoring via Netifyd DPI'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
|
||||
E('div', {
|
||||
'class': 'alert alert-info',
|
||||
'style': 'margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem'
|
||||
'style': 'display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 24px; font-size: 13px; font-weight: 600; ' + (serviceRunning ? 'background: rgba(34,197,94,0.15); color: #16a34a' : 'background: rgba(239,68,68,0.15); color: #dc2626')
|
||||
}, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
|
||||
E('div', { 'style': 'width: 12px; height: 12px; background: #ef4444; border-radius: 2px' }),
|
||||
E('span', _('Upload'))
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem' }, [
|
||||
E('div', { 'style': 'width: 12px; height: 12px; background: #10b981; border-radius: 2px' }),
|
||||
E('span', _('Download'))
|
||||
]),
|
||||
E('span', { 'class': 'text-muted' }, '|'),
|
||||
E('span', [
|
||||
E('i', { 'class': 'fa fa-sync' }),
|
||||
' ',
|
||||
_('Auto-refresh: Every %d seconds').format(this.refreshInterval)
|
||||
])
|
||||
E('span', { 'style': 'width: 8px; height: 8px; border-radius: 50%; background: ' + (serviceRunning ? '#22c55e' : '#ef4444') + '; animation: ' + (serviceRunning ? 'pulse 2s infinite' : 'none') }),
|
||||
serviceRunning ? _('Live') : _('Offline')
|
||||
]),
|
||||
E('div', { 'id': 'devices-table-container' })
|
||||
E('button', {
|
||||
'style': 'display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #6366f1; color: white; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer',
|
||||
'click': ui.createHandlerFn(this, 'handleExport')
|
||||
}, [
|
||||
'📥',
|
||||
_('Export')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Summary Cards
|
||||
E('div', { 'id': 'summary-cards-container' }, this.renderSummaryCards(this.devicesData).childNodes),
|
||||
|
||||
// Device List Header
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px' }, [
|
||||
E('h3', { 'style': 'margin: 0; font-size: 18px; font-weight: 600; color: #374151' }, _('Device List')),
|
||||
E('span', {
|
||||
'id': 'device-count-badge',
|
||||
'style': 'background: #6366f1; color: white; padding: 4px 12px; border-radius: 12px; font-size: 13px; font-weight: 600'
|
||||
}, this.devicesData.length)
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; gap: 12px; align-items: center' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 16px; font-size: 12px; color: #6b7280' }, [
|
||||
E('span', { 'style': 'display: flex; align-items: center; gap: 4px' }, [
|
||||
E('span', { 'style': 'width: 10px; height: 10px; background: linear-gradient(90deg, #ef4444, #f87171); border-radius: 2px' }),
|
||||
_('Upload')
|
||||
]),
|
||||
E('span', { 'style': 'display: flex; align-items: center; gap: 4px' }, [
|
||||
E('span', { 'style': 'width: 10px; height: 10px; background: linear-gradient(90deg, #22c55e, #4ade80); border-radius: 2px' }),
|
||||
_('Download')
|
||||
]),
|
||||
E('span', { 'style': 'color: #9ca3af' }, '|'),
|
||||
E('span', {}, '🔄 ' + _('%ds refresh').format(this.refreshInterval))
|
||||
]),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'placeholder': _('🔍 Search IP or MAC...'),
|
||||
'style': 'padding: 8px 16px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 13px; min-width: 200px; outline: none',
|
||||
'value': this.searchQuery,
|
||||
'keyup': function(ev) {
|
||||
self.searchQuery = ev.target.value;
|
||||
self.renderDevicesTable();
|
||||
}
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Devices Grid
|
||||
E('div', { 'id': 'devices-table-container' }),
|
||||
|
||||
// CSS for pulse animation
|
||||
E('style', {}, '@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }')
|
||||
]);
|
||||
},
|
||||
|
||||
|
||||
@ -22,3 +22,14 @@ config diagnostics 'settings'
|
||||
option health_threshold_cpu '80'
|
||||
option health_threshold_memory '90'
|
||||
option health_threshold_storage '85'
|
||||
|
||||
config wan_access 'remote'
|
||||
option enabled '1'
|
||||
option https_enabled '1'
|
||||
option https_port '443'
|
||||
option http_enabled '0'
|
||||
option http_port '80'
|
||||
option ssh_enabled '0'
|
||||
option ssh_port '22'
|
||||
option allowed_ips ''
|
||||
option dmz_mode '0'
|
||||
|
||||
@ -194,12 +194,30 @@ case "$1" in
|
||||
|
||||
json_add_object "update_component_settings"
|
||||
json_add_string "component_id" "string"
|
||||
json_add_object "settings" "object"
|
||||
json_add_object "settings"
|
||||
json_close_object
|
||||
json_close_object
|
||||
|
||||
json_add_object "sync_component_registry"
|
||||
json_close_object
|
||||
|
||||
# WAN Access management
|
||||
json_add_object "get_wan_access"
|
||||
json_close_object
|
||||
|
||||
json_add_object "set_wan_access"
|
||||
json_add_boolean "enabled" "boolean"
|
||||
json_add_boolean "https_enabled" "boolean"
|
||||
json_add_int "https_port" "integer"
|
||||
json_add_boolean "http_enabled" "boolean"
|
||||
json_add_int "http_port" "integer"
|
||||
json_add_boolean "ssh_enabled" "boolean"
|
||||
json_add_int "ssh_port" "integer"
|
||||
json_close_object
|
||||
|
||||
json_add_object "apply_wan_access"
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
@ -220,7 +238,7 @@ case "$1" in
|
||||
reload)
|
||||
/usr/sbin/secubox-core reload
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_dump
|
||||
;;
|
||||
|
||||
@ -587,12 +605,12 @@ case "$1" in
|
||||
# Use secubox-appstore for installation
|
||||
if /usr/sbin/secubox-appstore install "$app_id" >/dev/null 2>&1; then
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "App installed successfully"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Installation failed"
|
||||
json_add_string "details" "Check system logs for more information"
|
||||
json_dump
|
||||
@ -607,12 +625,12 @@ case "$1" in
|
||||
# Use secubox-appstore for removal
|
||||
if /usr/sbin/secubox-appstore remove "$app_id" >/dev/null 2>&1; then
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "App removed successfully"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Removal failed"
|
||||
json_add_string "details" "Check system logs for more information"
|
||||
json_dump
|
||||
@ -756,19 +774,19 @@ case "$1" in
|
||||
if uci set "${CONFIG_NAME}.${SECTION_NAME}.force_source=$source" >/dev/null 2>&1 && \
|
||||
uci commit "$CONFIG_NAME" >/dev/null 2>&1; then
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Catalog source set to: $source"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Failed to update UCI config"
|
||||
json_dump
|
||||
fi
|
||||
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "No source specified"
|
||||
json_dump
|
||||
fi
|
||||
@ -782,13 +800,13 @@ case "$1" in
|
||||
# Call secubox-catalog-sync (with or without source)
|
||||
if /usr/sbin/secubox-appstore sync ${source:+"$source"} 2>&1; then
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Catalog synced successfully"
|
||||
[ -n "$source" ] && json_add_string "source" "$source"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Sync failed"
|
||||
json_dump
|
||||
fi
|
||||
@ -942,12 +960,12 @@ case "$1" in
|
||||
|
||||
json_init
|
||||
if echo "$result" | grep -q "Success:"; then
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$result"
|
||||
json_add_string "component_id" "$component_id"
|
||||
json_add_string "new_state" "$new_state"
|
||||
else
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
@ -981,10 +999,10 @@ case "$1" in
|
||||
|
||||
json_init
|
||||
if echo "$result" | grep -q "Success:"; then
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$result"
|
||||
else
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
@ -998,10 +1016,10 @@ case "$1" in
|
||||
|
||||
json_init
|
||||
if echo "$result" | grep -q "Success:"; then
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$result"
|
||||
else
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
@ -1039,7 +1057,7 @@ case "$1" in
|
||||
# Extract settings object from input
|
||||
# This is simplified - full implementation would parse settings JSON
|
||||
json_init
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Settings update functionality available via CLI: secubox-component set-setting"
|
||||
json_dump
|
||||
;;
|
||||
@ -1049,15 +1067,53 @@ case "$1" in
|
||||
|
||||
json_init
|
||||
if echo "$result" | grep -q "successfully"; then
|
||||
json_add_boolean "success" true
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$result"
|
||||
else
|
||||
json_add_boolean "success" false
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_wan_access)
|
||||
/usr/sbin/secubox-wan-access json
|
||||
;;
|
||||
|
||||
set_wan_access)
|
||||
read -r input
|
||||
enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
|
||||
https_enabled=$(echo "$input" | jsonfilter -e '@.https_enabled' 2>/dev/null)
|
||||
https_port=$(echo "$input" | jsonfilter -e '@.https_port' 2>/dev/null)
|
||||
http_enabled=$(echo "$input" | jsonfilter -e '@.http_enabled' 2>/dev/null)
|
||||
http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null)
|
||||
ssh_enabled=$(echo "$input" | jsonfilter -e '@.ssh_enabled' 2>/dev/null)
|
||||
ssh_port=$(echo "$input" | jsonfilter -e '@.ssh_port' 2>/dev/null)
|
||||
|
||||
# Update UCI settings
|
||||
[ -n "$enabled" ] && uci set secubox.remote.enabled="$enabled"
|
||||
[ -n "$https_enabled" ] && uci set secubox.remote.https_enabled="$https_enabled"
|
||||
[ -n "$https_port" ] && uci set secubox.remote.https_port="$https_port"
|
||||
[ -n "$http_enabled" ] && uci set secubox.remote.http_enabled="$http_enabled"
|
||||
[ -n "$http_port" ] && uci set secubox.remote.http_port="$http_port"
|
||||
[ -n "$ssh_enabled" ] && uci set secubox.remote.ssh_enabled="$ssh_enabled"
|
||||
[ -n "$ssh_port" ] && uci set secubox.remote.ssh_port="$ssh_port"
|
||||
uci commit secubox
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "WAN access settings updated"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
apply_wan_access)
|
||||
/usr/sbin/secubox-wan-access apply >/dev/null 2>&1
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "WAN access rules applied"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
json_init
|
||||
json_add_boolean "error" true
|
||||
|
||||
@ -89,6 +89,23 @@ get_active_catalog() {
|
||||
echo "$MAIN_CATALOG"
|
||||
}
|
||||
|
||||
# Check if service is enabled (will start on boot)
|
||||
is_service_enabled() {
|
||||
local service="$1"
|
||||
[ -z "$service" ] && return 1
|
||||
[ -f "/etc/init.d/$service" ] || return 1
|
||||
# Check if service has symlink in /etc/rc.d/
|
||||
ls /etc/rc.d/S*"$service" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check if service is running/active
|
||||
is_service_active() {
|
||||
local service="$1"
|
||||
[ -z "$service" ] && return 1
|
||||
[ -f "/etc/init.d/$service" ] || return 1
|
||||
/etc/init.d/"$service" status >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# List all modules
|
||||
list_modules() {
|
||||
local format="${1:-table}"
|
||||
@ -117,15 +134,62 @@ list_modules() {
|
||||
local module_name=$(jsonfilter -i "$catalog" -e '@.name' 2>/dev/null)
|
||||
local module_category=$(jsonfilter -i "$catalog" -e '@.category' 2>/dev/null)
|
||||
local module_version=$(jsonfilter -i "$catalog" -e '@.version' 2>/dev/null)
|
||||
local service_name=$(jsonfilter -i "$catalog" -e '@.procd.service' 2>/dev/null)
|
||||
|
||||
# Check if installed
|
||||
# Auto-detect service name if not defined in catalog
|
||||
if [ -z "$service_name" ]; then
|
||||
case "$module_id" in
|
||||
*crowdsec*) service_name="crowdsec" ;;
|
||||
*netifyd*) service_name="netifyd" ;;
|
||||
*adguardhome*) service_name="AdGuardHome" ;;
|
||||
*wireguard*) service_name="wg-quick" ;;
|
||||
*nginx*|*vhost*) service_name="nginx" ;;
|
||||
*dnscrypt*) service_name="dnscrypt-proxy" ;;
|
||||
*unbound*) service_name="unbound" ;;
|
||||
*stubby*) service_name="stubby" ;;
|
||||
*shadowsocks*) service_name="shadowsocks-libev" ;;
|
||||
*zerotier*) service_name="zerotier" ;;
|
||||
*tailscale*) service_name="tailscale" ;;
|
||||
*docker*|*portainer*) service_name="dockerd" ;;
|
||||
*homeassistant*) service_name="homeassistant" ;;
|
||||
*jellyfin*) service_name="jellyfin" ;;
|
||||
*nextcloud*) service_name="nextcloud" ;;
|
||||
*uptimekuma*|*uptime-kuma*) service_name="uptime-kuma" ;;
|
||||
*vaultwarden*) service_name="vaultwarden" ;;
|
||||
*gitea*) service_name="gitea" ;;
|
||||
*domoticz*) service_name="domoticz" ;;
|
||||
*zigbee2mqtt*) service_name="zigbee2mqtt" ;;
|
||||
*mosquitto*|*mqtt*) service_name="mosquitto" ;;
|
||||
*netdata*) service_name="netdata" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Check if installed (support both packages.required[0] and packages[0] formats)
|
||||
local packages=$(jsonfilter -i "$catalog" -e '@.packages.required[0]' 2>/dev/null)
|
||||
[ -z "$packages" ] && packages=$(jsonfilter -i "$catalog" -e '@.packages[0]' 2>/dev/null)
|
||||
local status="available"
|
||||
local installed_version=""
|
||||
local is_installed=0
|
||||
local is_enabled=0
|
||||
local is_active=0
|
||||
|
||||
if [ "$cache_ready" -eq 1 ] && [ -n "$packages" ]; then
|
||||
installed_version=$(get_installed_version_from_cache "$packages")
|
||||
if [ -n "$installed_version" ]; then
|
||||
status="installed"
|
||||
is_installed=1
|
||||
|
||||
# Check enabled/active status if service defined
|
||||
if [ -n "$service_name" ]; then
|
||||
if is_service_enabled "$service_name"; then
|
||||
is_enabled=1
|
||||
status="enabled"
|
||||
fi
|
||||
if is_service_active "$service_name"; then
|
||||
is_active=1
|
||||
status="active"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -137,7 +201,10 @@ list_modules() {
|
||||
json_add_string "version" "$module_version"
|
||||
json_add_string "status" "$status"
|
||||
[ -n "$installed_version" ] && json_add_string "installed_version" "$installed_version"
|
||||
json_add_boolean "installed" $([ "$status" = "installed" ] && echo true || echo false)
|
||||
[ -n "$service_name" ] && json_add_string "service" "$service_name"
|
||||
json_add_boolean "installed" "$is_installed"
|
||||
json_add_boolean "enabled" "$is_enabled"
|
||||
json_add_boolean "active" "$is_active"
|
||||
json_close_object
|
||||
else
|
||||
printf "%-20s %-12s %-12s %-10s\n" \
|
||||
|
||||
235
package/secubox/secubox-core/root/usr/sbin/secubox-wan-access
Normal file
235
package/secubox/secubox-core/root/usr/sbin/secubox-wan-access
Normal file
@ -0,0 +1,235 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SecuBox WAN Access Manager
|
||||
# Manages firewall rules for remote access to LuCI/SecuBox
|
||||
#
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
RULE_PREFIX="secubox_wan"
|
||||
|
||||
# Remove all SecuBox WAN access rules
|
||||
remove_rules() {
|
||||
local changed=0
|
||||
|
||||
# Find and remove all secubox_wan rules
|
||||
while uci -q get firewall.@rule[-1] >/dev/null 2>&1; do
|
||||
local name=$(uci -q get firewall.@rule[-1].name)
|
||||
if echo "$name" | grep -q "^${RULE_PREFIX}"; then
|
||||
uci delete firewall.@rule[-1]
|
||||
changed=1
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Iterate through all rules to find secubox ones
|
||||
local i=0
|
||||
while true; do
|
||||
local name=$(uci -q get firewall.@rule[$i].name)
|
||||
[ -z "$name" ] && break
|
||||
|
||||
if echo "$name" | grep -q "^${RULE_PREFIX}"; then
|
||||
uci delete firewall.@rule[$i]
|
||||
changed=1
|
||||
# Don't increment i since indices shift after delete
|
||||
else
|
||||
i=$((i + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$changed" -eq 1 ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detect WAN zone name (fallback to '*' for any)
|
||||
get_wan_zone() {
|
||||
# Try common WAN zone names
|
||||
for zone in wan WAN external internet; do
|
||||
if uci -q get firewall.@zone[] 2>/dev/null | grep -q "name='$zone'"; then
|
||||
echo "$zone"
|
||||
return
|
||||
fi
|
||||
done
|
||||
# Check for zone with wan/wan6 network
|
||||
local i=0
|
||||
while true; do
|
||||
local name=$(uci -q get firewall.@zone[$i].name 2>/dev/null)
|
||||
[ -z "$name" ] && break
|
||||
local network=$(uci -q get firewall.@zone[$i].network 2>/dev/null)
|
||||
if echo "$network" | grep -qE '^wan|wan6|wan '; then
|
||||
echo "$name"
|
||||
return
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
# No WAN zone found - use '*' for any source
|
||||
echo "*"
|
||||
}
|
||||
|
||||
# Add a firewall rule
|
||||
add_rule() {
|
||||
local name="$1"
|
||||
local port="$2"
|
||||
local proto="${3:-tcp}"
|
||||
local src="${4:-$(get_wan_zone)}"
|
||||
|
||||
uci add firewall rule >/dev/null
|
||||
uci set firewall.@rule[-1].name="$name"
|
||||
uci set firewall.@rule[-1].src="$src"
|
||||
uci set firewall.@rule[-1].dest_port="$port"
|
||||
uci set firewall.@rule[-1].proto="$proto"
|
||||
uci set firewall.@rule[-1].target="ACCEPT"
|
||||
uci set firewall.@rule[-1].enabled="1"
|
||||
}
|
||||
|
||||
# Apply rules based on secubox config
|
||||
apply_rules() {
|
||||
config_load secubox
|
||||
|
||||
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
|
||||
|
||||
config_get enabled remote enabled "0"
|
||||
config_get https_enabled remote https_enabled "0"
|
||||
config_get https_port remote https_port "443"
|
||||
config_get http_enabled remote http_enabled "0"
|
||||
config_get http_port remote http_port "80"
|
||||
config_get ssh_enabled remote ssh_enabled "0"
|
||||
config_get ssh_port remote ssh_port "22"
|
||||
|
||||
# Remove existing rules first
|
||||
remove_rules
|
||||
|
||||
# Only add rules if WAN access is enabled
|
||||
if [ "$enabled" = "1" ]; then
|
||||
# HTTPS access
|
||||
if [ "$https_enabled" = "1" ]; then
|
||||
add_rule "${RULE_PREFIX}_https" "$https_port" "tcp" "wan"
|
||||
echo "Added HTTPS access rule (port $https_port)"
|
||||
fi
|
||||
|
||||
# HTTP access
|
||||
if [ "$http_enabled" = "1" ]; then
|
||||
add_rule "${RULE_PREFIX}_http" "$http_port" "tcp" "wan"
|
||||
echo "Added HTTP access rule (port $http_port)"
|
||||
fi
|
||||
|
||||
# SSH access
|
||||
if [ "$ssh_enabled" = "1" ]; then
|
||||
add_rule "${RULE_PREFIX}_ssh" "$ssh_port" "tcp" "wan"
|
||||
echo "Added SSH access rule (port $ssh_port)"
|
||||
fi
|
||||
fi
|
||||
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
|
||||
echo "WAN access rules applied"
|
||||
}
|
||||
|
||||
# Show current status
|
||||
status() {
|
||||
config_load secubox
|
||||
|
||||
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
|
||||
|
||||
config_get enabled remote enabled "0"
|
||||
config_get https_enabled remote https_enabled "0"
|
||||
config_get https_port remote https_port "443"
|
||||
config_get http_enabled remote http_enabled "0"
|
||||
config_get http_port remote http_port "80"
|
||||
config_get ssh_enabled remote ssh_enabled "0"
|
||||
config_get ssh_port remote ssh_port "22"
|
||||
|
||||
echo "SecuBox WAN Access Status"
|
||||
echo "========================="
|
||||
echo "Master switch: $([ "$enabled" = "1" ] && echo "ENABLED" || echo "DISABLED")"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " HTTPS (port $https_port): $([ "$https_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
|
||||
echo " HTTP (port $http_port): $([ "$http_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
|
||||
echo " SSH (port $ssh_port): $([ "$ssh_enabled" = "1" ] && echo "ENABLED" || echo "disabled")"
|
||||
echo ""
|
||||
echo "Active firewall rules:"
|
||||
iptables -L INPUT -n --line-numbers 2>/dev/null | grep -E "dpt:(${https_port}|${http_port}|${ssh_port})" || echo " (none)"
|
||||
}
|
||||
|
||||
# Enable WAN access
|
||||
enable() {
|
||||
uci set secubox.remote.enabled='1'
|
||||
uci commit secubox
|
||||
apply_rules
|
||||
echo "WAN access enabled"
|
||||
}
|
||||
|
||||
# Disable WAN access
|
||||
disable() {
|
||||
uci set secubox.remote.enabled='0'
|
||||
uci commit secubox
|
||||
remove_rules
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
echo "WAN access disabled"
|
||||
}
|
||||
|
||||
# JSON output for API
|
||||
json_status() {
|
||||
config_load secubox
|
||||
|
||||
local enabled https_enabled https_port http_enabled http_port ssh_enabled ssh_port
|
||||
|
||||
config_get enabled remote enabled "0"
|
||||
config_get https_enabled remote https_enabled "0"
|
||||
config_get https_port remote https_port "443"
|
||||
config_get http_enabled remote http_enabled "0"
|
||||
config_get http_port remote http_port "80"
|
||||
config_get ssh_enabled remote ssh_enabled "0"
|
||||
config_get ssh_port remote ssh_port "22"
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
json_init
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_object "services"
|
||||
json_add_object "https"
|
||||
json_add_boolean "enabled" "$https_enabled"
|
||||
json_add_int "port" "$https_port"
|
||||
json_close_object
|
||||
json_add_object "http"
|
||||
json_add_boolean "enabled" "$http_enabled"
|
||||
json_add_int "port" "$http_port"
|
||||
json_close_object
|
||||
json_add_object "ssh"
|
||||
json_add_boolean "enabled" "$ssh_enabled"
|
||||
json_add_int "port" "$ssh_port"
|
||||
json_close_object
|
||||
json_close_object
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
apply)
|
||||
apply_rules
|
||||
;;
|
||||
remove)
|
||||
remove_rules
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
echo "WAN access rules removed"
|
||||
;;
|
||||
enable)
|
||||
enable
|
||||
;;
|
||||
disable)
|
||||
disable
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
json)
|
||||
json_status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {apply|remove|enable|disable|status|json}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user