diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 159579a5..3553a8ec 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1,6 +1,6 @@ # SecuBox UI & Theme History -_Last updated: 2026-02-19_ +_Last updated: 2026-02-20_ 1. **Unified Dashboard Refresh (2025-12-20)** - Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs. @@ -2490,3 +2490,110 @@ git checkout HEAD -- index.html - UCI config section: `config recording 'recording'` with enabled/format/retention_days - Menu entry: Services → VoIP PBX → Recordings - Note: OVH SIP trunk registration requires correct password from OVH Manager + +41. **Matrix Homeserver Integration (2026-02-19)** + - Created `secubox-app-matrix` package for Conduit Matrix server: + - Lightweight Rust-based homeserver (~15MB binary, ~500MB RAM) + - LXC Debian Bookworm container with pre-built ARM64/x86_64 binaries + - E2EE messaging with federation support + - RocksDB database for performance + - New `matrixctl` CLI commands: + - `install`, `uninstall`, `update` - Container lifecycle + - `start`, `stop`, `restart`, `status` - Service control + - `user add/del/passwd/list` - User management + - `room list/create/delete` - Room management + - `federation test/status` - Federation testing + - `configure-haproxy`, `emancipate ` - Punk Exposure + - `identity link/unlink` - DID integration + - `mesh publish/unpublish` - P2P service registry + - `backup`, `restore` - Data persistence + - Created `luci-app-matrix` LuCI dashboard: + - Install wizard for new deployments + - Status card with running state, version, features + - Service controls (Start/Stop/Update/Uninstall) + - User management form + - Emancipate form for public exposure + - Identity integration section (DID linking) + - Mesh publication toggle + - Logs viewer with refresh + - RPCD methods (18 total): status, logs, start, stop, install, uninstall, update, + emancipate, configure_haproxy, user_add, user_del, federation_status, + identity_status, identity_link, identity_unlink, mesh_status, mesh_publish, mesh_unpublish + - UCI config sections: main, server, federation, admin, database, network, identity, mesh + - v1.0.0 roadmap: Matrix integration complements VoIP/Jabber for full mesh communication stack + - Files created: + - `package/secubox/secubox-app-matrix/` (Makefile, UCI, init.d, matrixctl) + - `package/secubox/luci-app-matrix/` (RPCD, ACL, menu, overview.js, api.js) + +25. **HexoJS KISS Static Upload & Multi-User Authentication (2026-02-20)** + - Added multi-user/multi-instance authentication: + - HAProxy Basic Auth integration with apr1 password hashing + - `hexoctl user add/del/passwd/list/grant/revoke` commands + - `hexoctl auth enable/disable/status/haproxy` commands + - UCI config sections for users and per-instance auth + - KISS Static Upload workflow (no Hexo build process): + - `hexoctl static create ` - Create static-only site + - `hexoctl static upload [inst]` - Upload HTML/CSS/JS directly + - `hexoctl static publish [inst]` - Copy to /www/ for uhttpd serving + - `hexoctl static quick [inst]` - One-command upload + publish + - `hexoctl static list [inst]` - List static files + - `hexoctl static serve [inst]` - Python/busybox httpd server + - `hexoctl static delete ` - Delete static instance + - Goal: Fast publishing experiment (KISSS) for HTML files without Node.js/Hexo build + - Tested and verified on router with immediate uhttpd serving + +26. **SaaS Relay CDN Caching & Session Replay (2026-02-20)** + - Enhanced `secubox-app-saas-relay` with CDN caching layer and multi-user session replay + - CDN Cache features: + - Configurable cache profiles: minimal, gandalf (default), aggressive + - Profile-based caching rules (content types, TTL, max size, exclude patterns) + - File-based cache storage with metadata for expiry tracking + - Cache-Control header respect (max-age, no-store, private) + - `X-SaaSRelay-Cache: HIT/MISS` header for debugging + - Session Replay features: + - Three modes: shared (default), per_user, master + - Shared mode: All SecuBox users share same session cookies + - Per-user mode: Each user gets their own session storage + - Master mode: One user (admin) authenticates, others replay their session + - New CLI commands: + - `saasctl cache {status|clear|profile|enable|disable}` - Cache management + - `saasctl session {status|mode|master|enable|disable}` - Session management + - Enhanced mitmproxy addon (415 lines) with: + - Response caching before network request + - Cache key generation with SHA-256 URL hashing + - Per-user session file storage with fallback to master + - Activity logging with emoji indicators + - UCI config sections added: cache, cache_profile (3), session_replay + - Config JSON export for container: config.json + services.json + +27. **Matrix Homeserver (Conduit) Integration (2026-02-20)** + - E2EE mesh messaging using Conduit Matrix homeserver (v0.10.12) + - `secubox-app-matrix` package with LXC container management: + - Pre-built ARM64 Conduit binary from GitLab artifacts + - Debian Bookworm base, RocksDB backend + - 512MB RAM limit, persistent data in /srv/matrix + - `matrixctl` CLI tool (1279 lines): + - Container: install, uninstall, update, check, shell + - Service: start, stop, restart, status, logs + - Users: add, del, passwd, list + - Rooms: list, create, delete + - Federation: test, status + - Exposure: configure-haproxy, emancipate + - Identity: link, unlink, status (DID integration) + - Mesh: publish, unpublish + - Backup: backup, restore + - `luci-app-matrix` dashboard: + - Install wizard for first-time setup + - Status cards with feature badges + - Service controls + - User management form + - Emancipate (public exposure) form + - Identity/DID linking section + - P2P mesh publication toggle + - Logs viewer with refresh + - RPCD methods (17 total): status, logs, start, stop, install, uninstall, update, + emancipate, configure_haproxy, user_add, user_del, federation_status, + identity_status, identity_link, identity_unlink, mesh_status, mesh_publish, mesh_unpublish + - UCI config sections: main, server, federation, admin, database, network, identity, mesh + - Matrix API responding with v1.1-v1.12 support + - Files: `package/secubox/secubox-app-matrix/`, `package/secubox/luci-app-matrix/` diff --git a/.claude/TODO.md b/.claude/TODO.md index edd69b75..244f0430 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -188,8 +188,8 @@ All cloud providers are **opt-in**. Offline resilience: local tier always active - [x] Config Advisor (ANSSI prep) — Done 2026-02-07 - [ ] P2P Mesh Intelligence - [ ] Factory auto-provisioning -- [ ] VoIP integration -- [ ] Matrix integration +- [x] VoIP integration — Done 2026-02-19 +- [x] Matrix integration — Done 2026-02-19 ### v1.1+ — Extended Mesh diff --git a/.claude/WIP.md b/.claude/WIP.md index 6979058c..1bb0fcea 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,6 +1,6 @@ # Work In Progress (Claude) -_Last updated: 2026-02-19 (v0.22.0 - VoIP + Jabber Integration)_ +_Last updated: 2026-02-20 (v0.23.0 - Matrix + SaaS Relay + Media Hub)_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -62,6 +62,52 @@ _Last updated: 2026-02-19 (v0.22.0 - VoIP + Jabber Integration)_ - Gossip-based exposure config sync via secubox-p2p - Created `luci-app-vortex-dns` dashboard +### Just Completed (2026-02-20) + +- **Matrix Homeserver (Conduit)** — DONE (2026-02-20) + - E2EE mesh messaging server using Conduit Matrix homeserver + - LXC container with pre-built ARM64 Conduit binary (0.10.12) + - `matrixctl` CLI (1279 lines): install/uninstall/update, user management, rooms, federation + - `luci-app-matrix` dashboard with: + - Install wizard, status cards, feature badges + - Service controls (Start/Stop/Update/Uninstall) + - User management form + - Emancipate (public exposure) with HAProxy + SSL + - Identity (DID) integration section + - P2P mesh publication toggle + - Logs viewer + - RPCD backend with 17 methods + - UCI config: main, server, federation, admin, database, network, identity, mesh + - Tested and verified on router (all checks pass, API responding) + +- **SaaS Relay CDN Caching & Session Replay** — DONE (2026-02-20) + - CDN cache with configurable profiles: minimal, gandalf, aggressive + - Session replay modes: shared (default), per_user, master + - New CLI commands: `saasctl cache {status|clear|profile|enable|disable}` + - New CLI commands: `saasctl session {status|mode|master|enable|disable}` + - Enhanced mitmproxy addon (415 lines) with response caching + - UCI config sections: cache, cache_profile (3), session_replay + - Config JSON export: config.json + services.json + +- **Media Services Hub Dashboard** — DONE (2026-02-20) + - Unified dashboard for all SecuBox media services at `/admin/services/media-hub` + - Category-organized cards: streaming, conferencing, apps, display, social, monitoring + - Service cards with status indicators, start/stop/restart controls + - RPCD backend querying 8 media services (Jellyfin, Lyrion, Jitsi, PeerTube, etc.) + - Files: `luci-app-media-hub` package + +- **HexoJS KISS Static Upload** — DONE (2026-02-20) + - Multi-user/multi-instance authentication with HAProxy Basic Auth + - UCI config for users, auth, and instances + - `hexoctl user add/del/passwd/grant/revoke` commands + - `hexoctl auth enable/disable/status/haproxy` commands + - KISS static upload workflow (no Hexo build required): + - `hexoctl static create ` - Create static-only site + - `hexoctl static upload ` - Upload HTML/CSS/JS directly + - `hexoctl static publish` - Copy to /www/ for immediate serving + - `hexoctl static quick ` - One-command upload + publish + - Tested and verified on router + ### Just Completed (2026-02-19) - **WAF VoIP/XMPP Security Filters** — DONE (2026-02-19) @@ -94,6 +140,16 @@ _Last updated: 2026-02-19 (v0.22.0 - VoIP + Jabber Integration)_ - Updated luci.jabber RPCD with 9 new VoIP methods - UCI config sections: jingle, sms, voicemail +- **Matrix Homeserver Integration** — DONE (2026-02-19) + - Created `secubox-app-matrix` package with Conduit Matrix server in LXC + - Pre-built ARM64/x86_64 binaries (~15MB), ~500MB RAM footprint + - `matrixctl` CLI: install/start/stop, user management, federation, emancipate + - HAProxy integration, identity linking (DID), P2P mesh publication + - Created `luci-app-matrix` dashboard with KISS theme + - Install wizard, status cards, user form, emancipate form, logs viewer + - RPCD backend with 18 methods + - Completes v1.0.0 roadmap: Matrix + VoIP + Jabber = full mesh communication stack + ### Just Completed (2026-02-17) - **PeerTube yt-dlp Video Import** — DONE (2026-02-17) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 041ebf21..b62655f3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -353,7 +353,29 @@ "Bash(gh repo view:*)", "Bash(gh repo edit:*)", "Bash(gh auth:*)", - "WebFetch(domain:cloud.gk2.secubox.in)" + "WebFetch(domain:cloud.gk2.secubox.in)", + "WebFetch(domain:docs.conduit.rs)", + "Bash(' -f1\\)\n local nc_ok=$\\(echo \"\"$nc_stats\"\" | cut -d')", + "Bash(' -f2\\)\n local nc_fail=$\\(echo \"\"$nc_stats\"\" | cut -d')", + "Bash(' -f3\\)\n \n local pt_last=$\\(echo \"\"$pt_stats\"\" | cut -d')", + "Bash(' -f1\\)\n local pt_ok=$\\(echo \"\"$pt_stats\"\" | cut -d')", + "Bash(' -f2\\)\n local pt_fail=$\\(echo \"\"$pt_stats\"\" | cut -d')", + "Bash(' -f3\\)\n \n local jb_last=$\\(echo \"\"$jb_stats\"\" | cut -d')", + "Bash(' -f1\\)\n local jb_ok=$\\(echo \"\"$jb_stats\"\" | cut -d')", + "Bash(' -f2\\)\n local jb_fail=$\\(echo \"\"$jb_stats\"\" | cut -d')", + "Bash(' -f3\\)\n \n local mx_last=$\\(echo \"\"$mx_stats\"\" | cut -d')", + "Bash(' -f1\\)\n local mx_ok=$\\(echo \"\"$mx_stats\"\" | cut -d')", + "Bash(' -f2\\)\n local mx_fail=$\\(echo \"\"$mx_stats\"\" | cut -d')", + "Bash(' -f3\\)\n \n local em_last=$\\(echo \"\"$em_stats\"\" | cut -d')", + "Bash(' -f1\\)\n local em_ok=$\\(echo \"\"$em_stats\"\" | cut -d')", + "Bash(' -f2\\)\n local em_fail=$\\(echo \"\"$em_stats\"\" | cut -d')", + "Bash(' -f3\\)\n \n # Calculate totals\n local total_success=$\\(\\(${nc_ok:-0} + ${pt_ok:-0} + ${jb_ok:-0} + ${mx_ok:-0} + ${em_ok:-0}\\)\\)\n local total_failure=$\\(\\(${nc_fail:-0} + ${pt_fail:-0} + ${jb_fail:-0} + ${mx_fail:-0} + ${em_fail:-0}\\)\\)\n \n # Find most recent login\n local last_login=\"\"\"\"\n for ts in \"\"$nc_last\"\" \"\"$pt_last\"\" \"\"$jb_last\"\" \"\"$mx_last\"\" \"\"$em_last\"\"; do\n [ -n \"\"$ts\"\" ] && last_login=\"\"$ts\"\"\n done\n \n # Output JSON\n cat << EOFSTATS\n{\n \"\"last_login\"\": \"\"${last_login:-never}\"\",\n \"\"total_success\"\": $total_success,\n \"\"total_failure\"\": $total_failure,\n \"\"services\"\": {\n \"\"nextcloud\"\": {\"\"last\"\": \"\"${nc_last:-}\"\", \"\"success\"\": ${nc_ok:-0}, \"\"failure\"\": ${nc_fail:-0}},\n \"\"peertube\"\": {\"\"last\"\": \"\"${pt_last:-}\"\", \"\"success\"\": ${pt_ok:-0}, \"\"failure\"\": ${pt_fail:-0}},\n \"\"jabber\"\": {\"\"last\"\": \"\"${jb_last:-}\"\", \"\"success\"\": ${jb_ok:-0}, \"\"failure\"\": ${jb_fail:-0}},\n \"\"matrix\"\": {\"\"last\"\": \"\"${mx_last:-}\"\", \"\"success\"\": ${mx_ok:-0}, \"\"failure\"\": ${mx_fail:-0}},\n \"\"email\"\": {\"\"last\"\": \"\"${em_last:-}\"\", \"\"success\"\": ${em_ok:-0}, \"\"failure\"\": ${em_fail:-0}}\n }\n}\nEOFSTATS\n}\nEOF\nchmod +x /usr/lib/secubox/users-login-stats.sh\necho \"\"Login stats library created\"\"')", + "WebFetch(domain:cdnjs.com)", + "Bash(npm install:*)", + "Bash(npm run build:*)", + "Bash(npx gulp browserify:*)", + "Bash(npx terser:*)", + "Bash(read)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 95b5ce73..bc063ba5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,24 @@ **When the user says "continue" or "next"**, consult WIP.md "Next Up" and TODO.md "Open" to pick the next task. When completing work, update these files to keep them current. New features and fixes must be appended to HISTORY.md with the date. +## Security Policies + +### WAF Bypass Prohibition +- **NEVER set `waf_bypass='1'` on any HAProxy vhost** — All traffic MUST pass through the mitmproxy WAF for inspection +- When adding new services/domains to HAProxy, route through `mitmproxy_inspector` backend and add the upstream route to `/srv/mitmproxy/haproxy-routes.json` +- If a service needs special handling (WebSocket, long connections), configure mitmproxy to properly forward the traffic rather than bypassing WAF +- Use `mitmproxyctl sync-routes` to regenerate routes from HAProxy backends, then manually add any missing routes for backends that don't have standard server entries + +### Mitmproxy Route Configuration +When adding a new service that routes through HAProxy → mitmproxy: +1. Add vhost: `haproxyctl vhost add ` +2. Backend will default to `mitmproxy_inspector` (correct) +3. Add route to mitmproxy: Edit `/srv/mitmproxy/haproxy-routes.json` and `/srv/mitmproxy-in/haproxy-routes.json`: + ```json + "domain.example.com": ["127.0.0.1", PORT] + ``` +4. Restart mitmproxy: `/etc/init.d/mitmproxy restart` + ## OpenWrt Shell Scripting Guidelines ### Process Detection diff --git a/luci-app-secubox-users/Makefile b/luci-app-secubox-users/Makefile new file mode 100644 index 00000000..42e21b53 --- /dev/null +++ b/luci-app-secubox-users/Makefile @@ -0,0 +1,24 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-secubox-users +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +LUCI_TITLE:=LuCI SecuBox User Management +LUCI_DEPENDS:=+secubox-core-users +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-secubox-users/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-users $(1)/usr/libexec/rpcd/luci.secubox-users + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-secubox-users.json $(1)/usr/share/rpcd/acl.d/luci-app-secubox-users.json + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-secubox-users.json $(1)/usr/share/luci/menu.d/luci-app-secubox-users.json + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/secubox-users + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-users/overview.js $(1)/www/luci-static/resources/view/secubox-users/overview.js +endef + +$(eval $(call BuildPackage,luci-app-secubox-users)) diff --git a/luci-app-secubox-users/build/aarch64_cortex-a72/secubox-app-bonus_0.3.0-r2_all.ipk b/luci-app-secubox-users/build/aarch64_cortex-a72/secubox-app-bonus_0.3.0-r2_all.ipk new file mode 100644 index 00000000..e8e62b21 Binary files /dev/null and b/luci-app-secubox-users/build/aarch64_cortex-a72/secubox-app-bonus_0.3.0-r2_all.ipk differ diff --git a/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js b/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js new file mode 100644 index 00000000..7ebde7ad --- /dev/null +++ b/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js @@ -0,0 +1,284 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require rpc'; + +var callStatus = rpc.declare({ + object: 'luci.secubox-users', + method: 'status', + expect: { } +}); + +var callUsers = rpc.declare({ + object: 'luci.secubox-users', + method: 'users', + expect: { } +}); + +var callAddUser = rpc.declare({ + object: 'luci.secubox-users', + method: 'add', + params: ['username', 'password', 'services'], + expect: { } +}); + +var callDeleteUser = rpc.declare({ + object: 'luci.secubox-users', + method: 'delete', + params: ['username'], + expect: { } +}); + +var callPasswd = rpc.declare({ + object: 'luci.secubox-users', + method: 'passwd', + params: ['username', 'password'], + expect: { } +}); + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callUsers() + ]); + }, + + renderServiceBadge: function(name, running) { + var color = running ? '#4CAF50' : '#9e9e9e'; + return E('span', { + 'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:' + color + ';font-size:0.85em;' + }, name); + }, + + renderUserRow: function(user) { + var self = this; + var services = (user.services || []).map(function(s) { + return E('span', { + 'style': 'display:inline-block;padding:1px 6px;margin:1px;border-radius:3px;background:#e3f2fd;font-size:0.8em;' + }, s); + }); + + return E('tr', {}, [ + E('td', {}, user.username), + E('td', {}, user.email), + E('td', {}, services), + E('td', {}, user.enabled === '1' ? 'Yes' : 'No'), + E('td', {}, [ + E('button', { + 'class': 'btn cbi-button', + 'click': function() { self.handlePasswd(user.username); }, + 'style': 'margin-right:5px;' + }, _('Password')), + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': function() { self.handleDelete(user.username); } + }, _('Delete')) + ]) + ]); + }, + + handleAdd: function() { + var self = this; + + ui.showModal(_('Add User'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Username')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'text', 'id': 'new-username', 'style': 'width:200px;' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Password')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'password', 'id': 'new-password', 'placeholder': _('Leave empty to generate'), 'style': 'width:200px;' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Services')), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', {}, [E('input', { 'type': 'checkbox', 'id': 'svc-nextcloud', 'checked': true }), ' Nextcloud']), + E('label', { 'style': 'margin-left:10px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-peertube', 'checked': true }), ' PeerTube']), + E('label', { 'style': 'margin-left:10px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-jabber', 'checked': true }), ' Jabber']), + E('label', { 'style': 'margin-left:10px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-matrix', 'checked': true }), ' Matrix']), + E('label', { 'style': 'margin-left:10px;' }, [E('input', { 'type': 'checkbox', 'id': 'svc-email', 'checked': true }), ' Email']) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var username = document.getElementById('new-username').value; + var password = document.getElementById('new-password').value; + var services = []; + if (document.getElementById('svc-nextcloud').checked) services.push('nextcloud'); + if (document.getElementById('svc-peertube').checked) services.push('peertube'); + if (document.getElementById('svc-jabber').checked) services.push('jabber'); + if (document.getElementById('svc-matrix').checked) services.push('matrix'); + if (document.getElementById('svc-email').checked) services.push('email'); + + if (!username) { + ui.addNotification(null, E('p', {}, _('Username required')), 'error'); + return; + } + + ui.hideModal(); + ui.showModal(_('Creating User...'), [ + E('p', { 'class': 'spinning' }, _('Please wait...')) + ]); + + callAddUser(username, password, services.join(',')).then(function(res) { + ui.hideModal(); + if (res && res.success) { + ui.showModal(_('User Created'), [ + E('div', { 'style': 'padding:20px;' }, [ + E('p', {}, _('User created successfully!')), + E('table', { 'style': 'margin:15px 0;' }, [ + E('tr', {}, [E('td', { 'style': 'padding:5px;font-weight:bold;' }, _('Username:')), E('td', { 'style': 'padding:5px;' }, res.username)]), + E('tr', {}, [E('td', { 'style': 'padding:5px;font-weight:bold;' }, _('Password:')), E('td', { 'style': 'padding:5px;font-family:monospace;background:#f5f5f5;' }, res.password)]), + E('tr', {}, [E('td', { 'style': 'padding:5px;font-weight:bold;' }, _('Email:')), E('td', { 'style': 'padding:5px;' }, res.email)]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn cbi-button-positive', 'click': function() { ui.hideModal(); location.reload(); } }, _('OK')) + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error'); + } + }); + }, + 'style': 'margin-left:10px;' + }, _('Create User')) + ]) + ]); + }, + + handleDelete: function(username) { + var self = this; + + if (!confirm(_('Delete user "%s" from all services?').format(username))) { + return; + } + + ui.showModal(_('Deleting...'), [ + E('p', { 'class': 'spinning' }, _('Please wait...')) + ]); + + callDeleteUser(username).then(function(res) { + ui.hideModal(); + if (res && res.success) { + ui.addNotification(null, E('p', {}, _('User deleted')), 'success'); + location.reload(); + } else { + ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error'); + } + }); + }, + + handlePasswd: function(username) { + var self = this; + + ui.showModal(_('Change Password'), [ + E('div', { 'class': 'cbi-section' }, [ + E('p', {}, _('Change password for: %s').format(username)), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('New Password')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'password', 'id': 'new-passwd', 'placeholder': _('Leave empty to generate'), 'style': 'width:200px;' }) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { + var password = document.getElementById('new-passwd').value; + ui.hideModal(); + ui.showModal(_('Updating...'), [ + E('p', { 'class': 'spinning' }, _('Please wait...')) + ]); + + callPasswd(username, password).then(function(res) { + ui.hideModal(); + if (res && res.success) { + ui.showModal(_('Password Updated'), [ + E('div', { 'style': 'padding:20px;' }, [ + E('p', {}, _('Password updated for all services!')), + E('p', { 'style': 'font-family:monospace;background:#f5f5f5;padding:10px;margin:10px 0;' }, res.password) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn cbi-button-positive', 'click': ui.hideModal }, _('OK')) + ]) + ]); + } else { + ui.addNotification(null, E('p', {}, _('Error updating password')), 'error'); + } + }); + }, + 'style': 'margin-left:10px;' + }, _('Update')) + ]) + ]); + }, + + render: function(data) { + var self = this; + var status = data[0] || {}; + var usersData = data[1] || {}; + var users = usersData.users || []; + var services = status.services || {}; + + var content = []; + + // Header + content.push(E('h2', {}, _('SecuBox User Management'))); + + // Status Section + content.push(E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Services')), + E('div', { 'style': 'margin:10px 0;' }, [ + this.renderServiceBadge('Nextcloud', services.nextcloud), + this.renderServiceBadge('PeerTube', services.peertube), + this.renderServiceBadge('Matrix', services.matrix), + this.renderServiceBadge('Jabber', services.jabber), + this.renderServiceBadge('Email', services.email) + ]), + E('p', { 'style': 'color:#666;' }, _('Domain: %s | Users: %d').format(status.domain, status.user_count)) + ])); + + // Users Table + var userRows = users.map(function(u) { return self.renderUserRow(u); }); + + content.push(E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Users')), + E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom:15px;' }, [ + E('button', { + 'class': 'btn cbi-button-positive', + 'click': function() { self.handleAdd(); } + }, _('Add User')) + ]), + users.length > 0 ? + E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Username')), + E('th', { 'class': 'th' }, _('Email')), + E('th', { 'class': 'th' }, _('Services')), + E('th', { 'class': 'th' }, _('Enabled')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ].concat(userRows)) : + E('p', { 'style': 'color:#666;' }, _('No users configured. Click "Add User" to create one.')) + ])); + + return E('div', { 'class': 'cbi-map' }, content); + } +}); diff --git a/luci-app-secubox-users/local-build.sh b/luci-app-secubox-users/local-build.sh new file mode 100755 index 00000000..d39e878e --- /dev/null +++ b/luci-app-secubox-users/local-build.sh @@ -0,0 +1,2917 @@ +#!/bin/bash +# +# local-build.sh - Local build script for SecuBox packages +# Replicates GitHub Actions workflows for local testing +# +# Usage: +# ./local-build.sh validate # Run validation only +# ./local-build.sh build # Build all packages (x86_64) +# ./local-build.sh build luci-app-system-hub # Build single package +# ./local-build.sh build secubox-core # Build SecuBox Core package +# ./local-build.sh build netifyd # Build netifyd DPI engine +# ./local-build.sh build --arch aarch64 # Build for specific architecture +# ./local-build.sh full # Validate + Build +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Normalize important directories +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) + +# Configuration +# Available versions: 25.12.0-rc1 (default), 24.10.5 (stable LTS), 23.05.5, SNAPSHOT +OPENWRT_VERSION="${OPENWRT_VERSION:-24.10.5}" +SDK_DIR="${SDK_DIR:-./sdk}" +BUILD_DIR="${BUILD_DIR:-./build}" +CACHE_DIR="${CACHE_DIR:-./cache}" +OPENWRT_DIR="${OPENWRT_DIR:-./openwrt}" + +# Default architecture +ARCH="aarch64_cortex-a72" +ARCH_NAME="aarch64_cortex-a72" +SDK_PATH="mvebu/cortexa72" + +# Device profiles for firmware building +declare -A DEVICE_PROFILES=( + ["espressobin-v7"]="mvebu:cortexa53:globalscale_espressobin:ESPRESSObin V7 (1-2GB DDR4)" + ["espressobin-ultra"]="mvebu:cortexa53:globalscale_espressobin-ultra:ESPRESSObin Ultra (PoE, WiFi)" + # ["sheeva64"]="mvebu:cortexa53:globalscale_sheeva64:Sheeva64 (Plug computer)" # Disabled + ["mochabin"]="mvebu:cortexa72:globalscale_mochabin:MOCHAbin (Quad-core A72, 10G)" + ["x86-64"]="x86:64:generic:x86_64 Generic PC" +) + +# Packages that must be built in the OpenWrt buildroot (toolchain) instead of the SDK. +# These packages compile native code and need system libraries not available in SDK. +# NOTE: secubox-app-* wrappers are PKGARCH:=all (shell scripts) and CAN be built in SDK. +# Include both shorthand names and full directory names for easy CLI usage. +OPENWRT_ONLY_PACKAGES=( + # C/C++ native binaries + "netifyd" # Shorthand + "secubox-app-netifyd" # C++ native binary wrapper + "ndpid" # Shorthand + "secubox-app-ndpid" # C++ native binary wrapper + "nodogsplash" # Shorthand + "secubox-app-nodogsplash" # C native binary wrapper (needs microhttpd) + # Go binaries + "crowdsec" # Shorthand + "secubox-app-crowdsec" # Go binary wrapper + "crowdsec-firewall-bouncer" # Full name + "cs-firewall-bouncer" # Shorthand + "secubox-app-cs-firewall-bouncer" # Go binary wrapper + # Python/special packages + "mitmproxy" # Shorthand + "secubox-app-mitmproxy" # Binary download package + "metablogizer" # Shorthand + "secubox-app-metablogizer" # Python dependencies + "tor" # Shorthand + "luci-app-tor" # Requires tor daemon compilation + "secubox-app-tor" # Tor service wrapper +) + +# Helper functions + +is_openwrt_only_pkg() { + local target="$1" + for pkg in "${OPENWRT_ONLY_PACKAGES[@]}"; do + if [[ "$pkg" == "$target" ]]; then + return 0 + fi + done + return 1 +} +print_header() { + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN}$1${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +# Architecture mapping +set_architecture() { + case "$1" in + x86-64|x86_64) + ARCH="x86-64" + ARCH_NAME="x86_64" + SDK_PATH="x86/64" + ;; + aarch64-cortex-a53|aarch64_cortex-a53) + ARCH="aarch64-cortex-a53" + ARCH_NAME="aarch64_cortex-a53" + SDK_PATH="mvebu/cortexa53" + ;; + aarch64-cortex-a72|aarch64_cortex-a72) + ARCH="aarch64-cortex-a72" + ARCH_NAME="aarch64_cortex-a72" + SDK_PATH="mvebu/cortexa72" + ;; + aarch64-generic|aarch64_generic) + ARCH="aarch64-generic" + ARCH_NAME="aarch64_generic" + SDK_PATH="armsr/armv8" + ;; + mips-24kc|mips_24kc) + ARCH="mips-24kc" + ARCH_NAME="mips_24kc" + SDK_PATH="ath79/generic" + ;; + mipsel-24kc|mipsel_24kc) + ARCH="mipsel-24kc" + ARCH_NAME="mipsel_24kc" + SDK_PATH="ramips/mt7621" + ;; + *) + print_error "Unknown architecture: $1" + print_info "Supported architectures: x86-64, aarch64-cortex-a53, aarch64-cortex-a72, aarch64-generic, mips-24kc, mipsel-24kc" + exit 1 + ;; + esac + print_info "Architecture: $ARCH ($ARCH_NAME) - SDK: $SDK_PATH" +} + +# Check dependencies +check_dependencies() { + print_header "Checking Dependencies" + + local missing_deps=() + + # Build tools + for cmd in make gcc g++ git wget curl tar xz jq ninja; do + if ! command -v "$cmd" &> /dev/null; then + missing_deps+=("$cmd") + fi + done + + # Validation tools + for cmd in shellcheck node; do + if ! command -v "$cmd" &> /dev/null; then + print_warning "$cmd not found (optional, needed for validation)" + fi + done + + if [ ${#missing_deps[@]} -gt 0 ]; then + print_error "Missing required dependencies: ${missing_deps[*]}" + echo "" + echo "Install them with:" + echo " sudo apt-get install -y build-essential clang flex bison g++ gawk \\" + echo " gcc-multilib g++-multilib gettext git libncurses5-dev \\" + echo " libssl-dev python3-setuptools python3-dev rsync \\" + echo " swig unzip zlib1g-dev file wget curl jq ninja-build" + echo "" + echo "For validation tools:" + echo " sudo apt-get install -y shellcheck nodejs" + exit 1 + fi + + print_success "All required dependencies found" +} + +# Validation functions (from test-validate.yml) +validate_makefiles() { + print_header "Validating Makefiles" + + local errors=0 + + # Validate luci-app-* packages + for makefile in ../luci-app-*/Makefile; do + if [[ -f "$makefile" ]]; then + local pkg=$(dirname "$makefile" | xargs basename) + echo " 🔍 Checking $pkg..." + + # Required fields + local required_fields=("PKG_NAME" "PKG_VERSION" "PKG_RELEASE" "PKG_LICENSE") + + for field in "${required_fields[@]}"; do + if ! grep -q "^${field}:=" "$makefile"; then + print_error "Missing: $field in $pkg" + errors=$((errors + 1)) + fi + done + + # Check for include statements + if ! grep -q "include.*luci.mk\|include.*package.mk" "$makefile"; then + print_error "Missing include statement in $pkg" + errors=$((errors + 1)) + fi + fi + done + + # Validate luci-theme-* packages + for makefile in ../luci-theme-*/Makefile; do + if [[ -f "$makefile" ]]; then + local pkg=$(dirname "$makefile" | xargs basename) + echo " 🔍 Checking $pkg..." + + # Required fields + local required_fields=("PKG_NAME" "PKG_VERSION" "PKG_RELEASE" "PKG_LICENSE") + + for field in "${required_fields[@]}"; do + if ! grep -q "^${field}:=" "$makefile"; then + print_error "Missing: $field in $pkg" + errors=$((errors + 1)) + fi + done + + # Check for include statements + if ! grep -q "include.*luci.mk\|include.*package.mk" "$makefile"; then + print_error "Missing include statement in $pkg" + errors=$((errors + 1)) + fi + fi + done + + if [[ $errors -gt 0 ]]; then + print_error "Found $errors Makefile errors" + return 1 + fi + + print_success "All Makefiles valid" + return 0 +} + +validate_json() { + print_header "Validating JSON Files" + + local errors=0 + + while IFS= read -r jsonfile; do + echo " 🔍 Checking $(basename "$jsonfile")..." + if ! jq empty "$jsonfile" 2>/dev/null; then + print_error "Invalid JSON: $jsonfile" + errors=$((errors + 1)) + fi + done < <(find .. -name "*.json" -type f ! -path "*/node_modules/*" ! -path "*/sdk/*" ! -path "*/build/*") + + if [[ $errors -gt 0 ]]; then + print_error "Found $errors JSON errors" + return 1 + fi + + print_success "All JSON files valid" + return 0 +} + +validate_javascript() { + print_header "Validating JavaScript Files" + + if ! command -v node &> /dev/null; then + print_warning "Node.js not found, skipping JavaScript validation" + return 0 + fi + + local errors=0 + + while IFS= read -r jsfile; do + echo " 🔍 Checking $(basename "$jsfile")..." + if ! node --check "$jsfile" 2>/dev/null; then + print_error "Syntax error in: $jsfile" + errors=$((errors + 1)) + fi + done < <(find .. -name "*.js" -type f ! -path "*/node_modules/*" ! -path "*/sdk/*" ! -path "*/build/*") + + if [[ $errors -gt 0 ]]; then + print_error "Found $errors JavaScript errors" + return 1 + fi + + print_success "All JavaScript files valid" + return 0 +} + +validate_shellscripts() { + print_header "Validating Shell Scripts" + + if ! command -v shellcheck &> /dev/null; then + print_warning "shellcheck not found, skipping shell script validation" + return 0 + fi + + local warnings=0 + + # Check RPCD scripts + while IFS= read -r script; do + echo " 🔍 Checking $(basename "$script")..." + if ! shellcheck -s sh "$script" 2>/dev/null; then + warnings=$((warnings + 1)) + fi + done < <(find .. -path "*/rpcd/*" -type f ! -path "*/sdk/*" ! -path "*/build/*" 2>/dev/null) + + # Check init scripts + while IFS= read -r script; do + echo " 🔍 Checking $(basename "$script")..." + if ! shellcheck -s sh "$script" 2>/dev/null; then + warnings=$((warnings + 1)) + fi + done < <(find .. -path "*/init.d/*" -type f ! -path "*/sdk/*" ! -path "*/build/*" 2>/dev/null) + + if [[ $warnings -gt 0 ]]; then + print_warning "Found $warnings shellcheck warnings (non-blocking)" + fi + + print_success "Shell script validation complete" + return 0 +} + +check_file_permissions() { + print_header "Checking File Permissions" + + local errors=0 + + # RPCD scripts should be executable + while IFS= read -r script; do + if [[ ! -x "$script" ]]; then + print_warning "Not executable: $script (fixing...)" + chmod +x "$script" + errors=$((errors + 1)) + fi + done < <(find .. -path "*/usr/libexec/rpcd/*" -type f ! -path "*/sdk/*" ! -path "*/build/*" 2>/dev/null) + + # Init scripts should be executable + while IFS= read -r script; do + if [[ ! -x "$script" ]]; then + print_warning "Not executable: $script (fixing...)" + chmod +x "$script" + errors=$((errors + 1)) + fi + done < <(find .. -path "*/etc/init.d/*" -type f ! -path "*/sdk/*" ! -path "*/build/*" 2>/dev/null) + + if [[ $errors -gt 0 ]]; then + print_warning "Fixed $errors permission issues" + fi + + print_success "File permissions checked" + return 0 +} + +# Download and setup SDK +download_sdk() { + print_header "Downloading OpenWrt SDK" + + local base_url="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/${SDK_PATH}" + + print_info "OpenWrt version: $OPENWRT_VERSION" + print_info "Architecture: $ARCH" + print_info "SDK URL: $base_url" + + # Check cache + if [[ -d "$SDK_DIR" && -f "$SDK_DIR/.sdk_ready" ]]; then + local cached_version=$(cat "$SDK_DIR/.sdk_ready") + if [[ "$cached_version" == "${OPENWRT_VERSION}-${ARCH}" ]]; then + print_success "Using cached SDK: ${OPENWRT_VERSION}-${ARCH}" + return 0 + else + print_info "Cached SDK version mismatch, re-downloading..." + rm -rf "$SDK_DIR" + fi + fi + + # Find SDK filename + echo " 📥 Fetching SDK list..." + local sdk_file + sdk_file=$(curl -sL --retry 3 --retry-delay 5 "$base_url/" | grep -oP 'openwrt-sdk[^"<>]+\.tar\.(xz|zst)' | head -1) + + if [[ -z "$sdk_file" ]]; then + print_error "Could not find SDK at $base_url" + return 1 + fi + + print_info "Downloading: $sdk_file" + + # Download SDK + mkdir -p "$CACHE_DIR" + local sdk_archive="$CACHE_DIR/$sdk_file" + + if [[ ! -f "$sdk_archive" ]]; then + echo " Downloading SDK (this may take several minutes)..." + if wget --retry-connrefused --waitretry=5 --timeout=60 --progress=dot:mega \ + "${base_url}/${sdk_file}" -O "$sdk_archive" 2>&1 | grep --line-buffered '%'; then + print_success "Download complete" + else + # Fallback for older wget versions + wget --retry-connrefused --waitretry=5 --timeout=60 \ + "${base_url}/${sdk_file}" -O "$sdk_archive" + fi + else + print_info "Using cached archive: $sdk_file" + fi + + # Extract SDK + print_info "Extracting SDK..." + rm -rf "$SDK_DIR" + mkdir -p "$SDK_DIR" + tar -xf "$sdk_archive" -C "$SDK_DIR" --strip-components=1 + + # Mark SDK as ready + echo "${OPENWRT_VERSION}-${ARCH}" > "$SDK_DIR/.sdk_ready" + + print_success "SDK downloaded and extracted" + return 0 +} + +# Setup SDK feeds +setup_sdk_feeds() { + print_header "Setting up SDK Feeds" + + cd "$SDK_DIR" + + # Remove unwanted feeds from feeds.conf.default + if [[ -f "feeds.conf.default" ]]; then + sed -i '/telephony/d' feeds.conf.default + sed -i '/routing/d' feeds.conf.default + print_success "Removed telephony and routing from feeds.conf.default" + fi + + # Create local feed for SecuBox packages outside of SDK + local local_feed_dir="$(pwd)/../local-feed" + mkdir -p "$local_feed_dir" + + # Determine correct branch based on OpenWrt version + local branch + if [[ "$OPENWRT_VERSION" == "SNAPSHOT" ]]; then + branch="master" + elif [[ "$OPENWRT_VERSION" =~ ^25\. ]]; then + branch="openwrt-25.12" + elif [[ "$OPENWRT_VERSION" =~ ^24\. ]]; then + branch="openwrt-24.10" + elif [[ "$OPENWRT_VERSION" =~ ^23\. ]]; then + branch="openwrt-23.05" + else + branch="openwrt-23.05" # fallback + fi + + print_info "Using branch: $branch for OpenWrt $OPENWRT_VERSION" + + # Use GitHub mirrors + local feed + cat > feeds.conf << FEEDS +src-git packages https://github.com/openwrt/packages.git;$branch +src-git luci https://github.com/openwrt/luci.git;$branch +src-link secubox $local_feed_dir +FEEDS + + print_info "feeds.conf configured with local SecuBox feed at $local_feed_dir" + + # Update feeds + echo "🔄 Updating feeds..." + local feeds_ok=0 + local required_feeds=3 + + for feed in packages luci secubox; do + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Updating feed: $feed" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + local feed_success=0 + for attempt in 1 2 3; do + echo "Attempt $attempt of 3..." + if ./scripts/feeds update "$feed" 2>&1 | tee "feed-update-${feed}.log"; then + if [[ -d "feeds/$feed" ]]; then + print_success "$feed updated successfully" + feeds_ok=$((feeds_ok + 1)) + feed_success=1 + break + else + print_warning "Feed directory not created, retrying..." + fi + else + print_warning "Update command failed, retrying..." + fi + sleep $((10 * attempt)) + done + + if [[ $feed_success -eq 0 ]]; then + print_error "Failed to update $feed after 3 attempts" + return 1 + fi + done + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📊 Feeds Status: $feeds_ok/$required_feeds updated" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + if [[ $feeds_ok -lt $required_feeds ]]; then + print_error "Not all required feeds were updated successfully" + return 1 + fi + + # Install feeds + echo "" + echo "📦 Installing feeds..." + if ! ./scripts/feeds install -a 2>&1 | tee feed-install.log; then + print_warning "Feed installation had errors, checking if critical..." + fi + + # Note: We skip Lua header installation and manual dependency builds + # Our SecuBox packages are PKGARCH:=all (scripts only) - no compilation needed + # lucihttp and cgi-io dependencies will be disabled in .config + echo "" + echo "ℹ️ Dependencies will be handled via .config (pre-built packages preferred)" + echo " Our packages are PKGARCH:=all (scripts) - no lucihttp compilation needed" + + # Verify feeds + echo "" + echo "🔍 Verifying feed installation..." + for feed in packages luci secubox; do + if [[ -d "feeds/$feed" ]]; then + local feed_size=$(du -sh "feeds/$feed" 2>/dev/null | cut -f1) + print_success "feeds/$feed exists ($feed_size)" + else + if [[ "$feed" == "secubox" ]]; then + print_warning "feeds/$feed is empty (will be populated)" + else + print_error "feeds/$feed is missing!" + return 1 + fi + fi + done + + # Verify luci.mk + if [[ ! -f "feeds/luci/luci.mk" ]]; then + print_warning "luci.mk not found, creating fallback..." + mkdir -p feeds/luci + cat > feeds/luci/luci.mk << 'LUCI_MK' +# Minimal LuCI build system fallback +LUCI_PKGARCH:=all + +define Package/Default + SECTION:=luci + CATEGORY:=LuCI + SUBMENU:=3. Applications + PKGARCH:=all +endef +LUCI_MK + fi + + # Final cleanup + rm -f feeds/telephony.index feeds/routing.index 2>/dev/null || true + rm -rf feeds/telephony feeds/routing 2>/dev/null || true + + make defconfig FORCE=1 2>/dev/null + + cd - > /dev/null + + print_success "SDK feeds configured" + return 0 +} + +# Synchronize packages from package/secubox to local-feed +# This ensures local-feed always matches the canonical source +sync_packages_to_local_feed() { + local feed_dir="./local-feed" + local pkg_src="$REPO_ROOT/package/secubox" + + print_info "Syncing from $pkg_src to $feed_dir" + + # Create local-feed if it doesn't exist + mkdir -p "$feed_dir" + + # Sync packages from package/secubox/ to local-feed + if [[ -d "$pkg_src" ]]; then + print_info "Syncing secubox core packages..." + for pkg in "$pkg_src"/*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📦 $pkg_name" + rm -rf "$feed_dir/$pkg_name" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include paths for feed structure + if grep -q "../../lang/golang/golang-package.mk" "$feed_dir/$pkg_name/Makefile" 2>/dev/null; then + sed -i 's|include.*../../lang/golang/golang-package.mk|include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk|' "$feed_dir/$pkg_name/Makefile" + echo " ✓ Fixed golang package include path" + fi + fi + done + fi + + # Sync luci-app-* packages from repo root + print_info "Syncing LuCI app packages..." + for pkg in "$REPO_ROOT"/luci-app-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📁 $pkg_name" + rm -rf "$feed_dir/$pkg_name" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include path for feed structure + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile" + fi + done + + # Sync luci-theme-* packages from repo root + print_info "Syncing LuCI theme packages..." + for pkg in "$REPO_ROOT"/luci-theme-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 🎨 $pkg_name" + rm -rf "$feed_dir/$pkg_name" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include path for feed structure + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile" + fi + done + + # Count packages + local pkg_count=$(ls -d "$feed_dir"/*/ 2>/dev/null | wc -l) + print_success "Synchronized $pkg_count packages to local-feed" +} + +# Deploy packages to router +deploy_packages() { + local router="$1" + local packages="$2" + local ssh_opts="-o StrictHostKeyChecking=no -o ConnectTimeout=10" + + # Test connectivity + print_info "Testing connection to router..." + if ! ssh $ssh_opts root@$router "echo 'Connected'" 2>/dev/null; then + print_error "Cannot connect to router at $router" + return 1 + fi + + # Find packages to deploy + local pkg_dir="$SDK_DIR/bin/packages/$ARCH_NAME/secubox" + local target_pkg_dir="$SDK_DIR/bin/targets/$SDK_PATH/packages" + + if [[ -n "$packages" ]]; then + # Deploy specific packages + print_info "Deploying specific packages: $packages" + for pkg in $packages; do + local ipk=$(find "$pkg_dir" "$target_pkg_dir" -name "${pkg}*.ipk" 2>/dev/null | head -1) + if [[ -n "$ipk" ]]; then + print_info "Deploying $(basename "$ipk")..." + scp $ssh_opts "$ipk" root@$router:/tmp/ + ssh $ssh_opts root@$router "opkg install /tmp/$(basename "$ipk") --force-reinstall 2>&1" + else + print_warning "Package not found: $pkg" + fi + done + else + # Deploy all recently built packages + print_info "Deploying all packages from SDK..." + + # Find all IPK files built today + local today=$(date +%Y%m%d) + local ipks=$(find "$pkg_dir" -name "*.ipk" -mtime 0 2>/dev/null) + + if [[ -z "$ipks" ]]; then + print_warning "No recently built packages found" + print_info "Run 'local-build.sh build ' first" + return 1 + fi + + # Copy packages + print_info "Copying packages to router..." + for ipk in $ipks; do + scp $ssh_opts "$ipk" root@$router:/tmp/ + done + + # Install packages + print_info "Installing packages..." + ssh $ssh_opts root@$router "opkg install /tmp/*.ipk --force-reinstall 2>&1" || true + fi + + # Sync feed to router + print_info "Syncing package feed to router..." + local feed_pkg="$SDK_DIR/bin/packages/$ARCH_NAME/secubox" + if [[ -d "$feed_pkg" ]]; then + ssh $ssh_opts root@$router "mkdir -p /www/secubox-feed" + scp $ssh_opts "$feed_pkg"/*.ipk root@$router:/www/secubox-feed/ 2>/dev/null || true + + # Generate Packages index + ssh $ssh_opts root@$router "cd /www/secubox-feed && \ + rm -f Packages Packages.gz && \ + for ipk in *.ipk; do \ + [ -f \"\$ipk\" ] && tar -xzf \"\$ipk\" ./control.tar.gz && \ + tar -xzf control.tar.gz ./control && \ + cat control >> Packages && echo '' >> Packages && \ + rm -f control control.tar.gz; \ + done && \ + gzip -k Packages 2>/dev/null || true" + + print_success "Feed synced to /www/secubox-feed" + fi + + print_success "Deployment complete" +} + +# Copy packages to SDK feed +copy_packages() { + local single_package="$1" + + print_header "Copying Packages to SecuBox Feed" + + cd "$SDK_DIR" + + # Use the local feed directory (outside SDK) + local feed_dir="../local-feed" + mkdir -p "$feed_dir" + local -a core_pkg_names=() + + if [[ -n "$single_package" ]]; then + print_info "Copying single package: $single_package" + + # Check in root directory first (luci-app-*, luci-theme-*) + if [[ -d "../../$single_package" && -f "../../${single_package}/Makefile" ]]; then + echo " 📁 $single_package" + cp -r "../../$single_package" "$feed_dir/" + + # Fix Makefile include path for LuCI packages + if [[ "$single_package" =~ ^luci- ]]; then + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$single_package/Makefile" + echo " ✓ Fixed Makefile include path" + fi + # Check in package/secubox/ directory (secubox-app-*, secubox-*) + elif [[ -d "../../package/secubox/$single_package" && -f "../../package/secubox/${single_package}/Makefile" ]]; then + echo " 📦 $single_package" + cp -r "../../package/secubox/$single_package" "$feed_dir/" + core_pkg_names+=("$single_package") + + # Fix Makefile include paths for feed structure + if grep -q "../../lang/golang/golang-package.mk" "$feed_dir/$single_package/Makefile" 2>/dev/null; then + sed -i 's|include.*../../lang/golang/golang-package.mk|include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk|' "$feed_dir/$single_package/Makefile" + echo " ✓ Fixed golang package include path" + fi + else + print_error "Package $single_package not found or missing Makefile" + cd - > /dev/null + return 1 + fi + else + print_info "Copying all packages" + + # Copy luci-app-* packages + for pkg in ../../luci-app-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📁 $pkg_name" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include path for feed structure + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile" + echo " ✓ Fixed Makefile include path" + fi + done + + # Copy luci-theme-* packages + for pkg in ../../luci-theme-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📁 $pkg_name" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include path for feed structure + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile" + echo " ✓ Fixed Makefile include path" + fi + done + + # Copy luci-app-* packages from package/secubox/ (e.g., luci-app-secubox-admin) + for pkg in ../../package/secubox/luci-app-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📁 $pkg_name (SecuBox LuCI)" + cp -r "$pkg" "$feed_dir/" + + # Fix Makefile include path for feed structure + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile" + echo " ✓ Fixed Makefile include path" + fi + done + + # Copy secubox-app-* packages (backend services) + for pkg in ../../package/secubox/secubox-app-*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📦 $pkg_name (SecuBox App)" + cp -r "$pkg" "$feed_dir/" + core_pkg_names+=("$pkg_name") + + # Fix Makefile include paths for feed structure + if grep -q "../../lang/golang/golang-package.mk" "$feed_dir/$pkg_name/Makefile" 2>/dev/null; then + sed -i 's|include.*../../lang/golang/golang-package.mk|include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk|' "$feed_dir/$pkg_name/Makefile" + echo " ✓ Fixed golang package include path" + fi + fi + done + + # Copy other core packages (non-LuCI, non-secubox-app) + for pkg in ../../package/secubox/*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + # Skip if already copied (luci-app-*, luci-theme-*, secubox-app-*) + if [[ ! "$pkg_name" =~ ^luci-app- ]] && \ + [[ ! "$pkg_name" =~ ^luci-theme- ]] && \ + [[ ! "$pkg_name" =~ ^secubox-app- ]] && \ + [[ "$pkg_name" != ".appstore" ]]; then + echo " 📁 $pkg_name (Core)" + cp -r "$pkg" "$feed_dir/" + core_pkg_names+=("$pkg_name") + fi + fi + done + fi + + echo "" + print_info "Packages in feed:" + ls -d "$feed_dir/luci-app-"*/ 2>/dev/null || true + ls -d "$feed_dir/luci-theme-"*/ 2>/dev/null || true + ls -d "$feed_dir/secubox-app-"*/ 2>/dev/null || true + + # Update the secubox feed + echo "" + echo "🔄 Updating SecuBox feed index..." + ./scripts/feeds update secubox + + # Install packages from secubox feed + echo "" + echo "📦 Installing packages from SecuBox feed..." + if [[ -n "$single_package" ]]; then + echo " Installing $single_package..." + ./scripts/feeds install "$single_package" + else + # Install luci-app-* packages + for pkg in "$feed_dir"/luci-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " Installing $pkg_name..." + ./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true + fi + done + + # Install luci-theme-* packages + for pkg in "$feed_dir"/luci-theme-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " Installing $pkg_name..." + ./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true + fi + done + + # Install secubox-app-* packages + for pkg in "$feed_dir"/secubox-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " Installing $pkg_name..." + ./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true + fi + done + + # Install secubox core packages + for pkg_name in "${core_pkg_names[@]}"; do + local pkg_path="$feed_dir/$pkg_name" + if [[ -d "$pkg_path" ]]; then + echo " Installing $pkg_name..." + # For netifyd, ensure we're using SecuBox feed (not packages feed which has old version) + if [[ "$pkg_name" == "netifyd" ]]; then + ./scripts/feeds uninstall netifyd 2>&1 | grep -v "WARNING:" || true + ./scripts/feeds install -p secubox netifyd 2>&1 | grep -v "WARNING:" || true + else + ./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true + fi + fi + done + fi + + cd - > /dev/null + + print_success "Packages copied and installed to feed" + return 0 +} + +# Configure packages +configure_packages() { + local single_package="$1" + + print_header "Configuring Packages" + + cd "$SDK_DIR" + + echo "⚙️ Enabling packages..." + + if [[ -n "$single_package" ]]; then + # Enable only the specified package + if [[ -d "feeds/secubox/$single_package" ]]; then + echo "CONFIG_PACKAGE_${single_package}=m" >> .config + print_success "$single_package enabled" + else + print_error "Package $single_package not found in feed" + cd - > /dev/null + return 1 + fi + else + # Enable all SecuBox packages from feed (luci-app-*) + for pkg in feeds/secubox/luci-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo "CONFIG_PACKAGE_${pkg_name}=m" >> .config + print_success "$pkg_name enabled" + fi + done + + # Enable all SecuBox theme packages from feed (luci-theme-*) + for pkg in feeds/secubox/luci-theme-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo "CONFIG_PACKAGE_${pkg_name}=m" >> .config + print_success "$pkg_name enabled" + fi + done + + # Enable all SecuBox app packages from feed (secubox-app-*) + for pkg in feeds/secubox/secubox-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo "CONFIG_PACKAGE_${pkg_name}=m" >> .config + print_success "$pkg_name enabled" + fi + done + fi + + # Disable problematic packages that fail to compile in SDK + # Our SecuBox packages are PKGARCH:=all (scripts) so they don't need these + echo "" + echo "⚠️ Disabling packages that fail in SDK environment..." + echo "# CONFIG_PACKAGE_lucihttp is not set" >> .config + echo "# CONFIG_PACKAGE_cgi-io is not set" >> .config + print_info "lucihttp and cgi-io disabled (fail to compile: missing lua.h)" + + # Enable use of pre-built packages from feeds + echo "CONFIG_DEVEL=y" >> .config + echo "CONFIG_AUTOREBUILD=y" >> .config + echo "CONFIG_AUTOREMOVE=y" >> .config + echo "CONFIG_FEED_packages=y" >> .config + echo "CONFIG_FEED_luci=y" >> .config + + make defconfig FORCE=1 2>/dev/null + + cd - > /dev/null + + print_success "Packages configured" + return 0 +} + +# Build packages +build_packages() { + local single_package="$1" + + print_header "Building Packages" + + cd "$SDK_DIR" + + # Detect package format based on OpenWrt version + local pkg_ext + if [[ "$OPENWRT_VERSION" =~ ^25\. ]] || [[ "$OPENWRT_VERSION" == "SNAPSHOT" ]]; then + pkg_ext="apk" + print_info "Building for OpenWrt $OPENWRT_VERSION (apk format)" + else + pkg_ext="ipk" + print_info "Building for OpenWrt $OPENWRT_VERSION (ipk format)" + fi + + # Export for later use + export PKG_EXT="$pkg_ext" + + local built=0 + local failed=0 + local built_list="" + local failed_list="" + + # Determine which packages to build + local packages_to_build=() + if [[ -n "$single_package" ]]; then + if [[ -d "feeds/secubox/$single_package" ]]; then + packages_to_build=("$single_package") + else + print_error "Package $single_package not found in feed" + cd - > /dev/null + return 1 + fi + else + # Build luci-app-* packages + for pkg in feeds/secubox/luci-app-*/; do + [[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")") + done + + # Build luci-theme-* packages + for pkg in feeds/secubox/luci-theme-*/; do + [[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")") + done + + # Build core secubox packages (secubox-app, nodogsplash, netifyd, etc.) + for pkg in feeds/secubox/secubox-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + if is_openwrt_only_pkg "$pkg_name"; then + print_info "Skipping $pkg_name (requires OpenWrt buildroot)" + continue + fi + packages_to_build+=("$pkg_name") + fi + done + for pkg in feeds/secubox/nodogsplash/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + if is_openwrt_only_pkg "$pkg_name"; then + print_info "Skipping $pkg_name (requires OpenWrt buildroot)" + continue + fi + packages_to_build+=("$pkg_name") + fi + done + for pkg in feeds/secubox/netifyd/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + if is_openwrt_only_pkg "$pkg_name"; then + print_info "Skipping $pkg_name (requires OpenWrt buildroot)" + continue + fi + packages_to_build+=("$pkg_name") + fi + done + fi + + # Build packages + for pkg_name in "${packages_to_build[@]}"; do + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📦 Building: $pkg_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Show package contents for debugging + echo "📁 Package contents:" + ls -la "feeds/secubox/$pkg_name" + + # Build with timeout (10 minutes per package) + local build_log="/tmp/build-${pkg_name}.log" + + # Build from feed (skip dependency checks for architecture-independent packages) + # These packages are just JavaScript/shell scripts - no compilation needed + if timeout 600 make "package/feeds/secubox/${pkg_name}/compile" V=s -j1 NO_DEPS=1 FORCE=1 > "$build_log" 2>&1; then + # Check if package was created (.apk or .ipk) + local pkg_file=$(find bin -name "${pkg_name}*.${pkg_ext}" 2>/dev/null | head -1) + + if [[ -n "$pkg_file" ]]; then + print_success "Built: $pkg_name" + echo " → $pkg_file" + built=$((built + 1)) + built_list="${built_list}${pkg_name}," + else + print_warning "No .${pkg_ext} generated for $pkg_name" + echo "📋 Last 50 lines of build log:" + tail -50 "$build_log" + failed=$((failed + 1)) + failed_list="${failed_list}${pkg_name}," + fi + else + print_error "Build failed: $pkg_name" + echo "📋 Last 100 lines of build log:" + tail -100 "$build_log" + failed=$((failed + 1)) + failed_list="${failed_list}${pkg_name}," + fi + + echo "" + done + + cd - > /dev/null + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📊 Build Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_success "Built: $built packages" + if [[ $failed -gt 0 ]]; then + print_error "Failed: $failed packages" + fi + echo "" + echo "Built: $built_list" + if [[ -n "$failed_list" ]]; then + echo "Failed: $failed_list" + fi + + return 0 +} + +# Collect artifacts +collect_artifacts() { + print_header "Collecting Artifacts" + + mkdir -p "$BUILD_DIR/$ARCH" + + # Use package extension from build step + local pkg_ext="${PKG_EXT:-ipk}" + print_info "Package format: .${pkg_ext}" + + # Find and copy package files (.apk or .ipk) + find "$SDK_DIR/bin" -name "luci-app-*.${pkg_ext}" -exec cp {} "$BUILD_DIR/$ARCH/" \; 2>/dev/null || true + find "$SDK_DIR/bin" -name "luci-theme-*.${pkg_ext}" -exec cp {} "$BUILD_DIR/$ARCH/" \; 2>/dev/null || true + + # Also collect any SecuBox related packages + find "$SDK_DIR/bin" -name "*secubox*.${pkg_ext}" -exec cp {} "$BUILD_DIR/$ARCH/" \; 2>/dev/null || true + find "$SDK_DIR/bin" -name "netifyd*.${pkg_ext}" -exec cp {} "$BUILD_DIR/$ARCH/" \; 2>/dev/null || true + + # Clean old versions, keep only latest + clean_old_ipk_versions "$BUILD_DIR/$ARCH" "$pkg_ext" + + # Count after cleanup + local pkg_count=$(find "$BUILD_DIR/$ARCH" -name "*.${pkg_ext}" 2>/dev/null | wc -l) + + echo "" + print_info "Built packages for $ARCH:" + ls -la "$BUILD_DIR/$ARCH/" 2>/dev/null || echo "No packages" + + # Create checksums + if [[ $pkg_count -gt 0 ]]; then + cd "$BUILD_DIR/$ARCH" + sha256sum ./*.${pkg_ext} > SHA256SUMS + cd - > /dev/null + fi + + echo "" + print_success "Total: $pkg_count packages" + + return 0 +} + +# Clean old IPK versions, keep only the latest for each package +clean_old_ipk_versions() { + local feed_dir="$1" + local pkg_ext="${2:-ipk}" + + print_info "Cleaning old package versions..." + + # Get list of all packages + local packages=() + for pkg in "$feed_dir"/*."$pkg_ext"; do + [[ -f "$pkg" ]] || continue + local basename=$(basename "$pkg") + # Extract package name (everything before first underscore) + local name=$(echo "$basename" | sed 's/_[0-9].*$//') + packages+=("$name") + done + + # Get unique package names + local unique_packages=($(printf '%s\n' "${packages[@]}" | sort -u)) + + local removed=0 + for name in "${unique_packages[@]}"; do + # Find all versions of this package, sorted by modification time (newest first) + local versions=($(ls -t "$feed_dir/${name}_"*."$pkg_ext" 2>/dev/null)) + + # Keep only the latest (first in the list), remove the rest + if [[ ${#versions[@]} -gt 1 ]]; then + for ((i=1; i<${#versions[@]}; i++)); do + echo " Removing old: $(basename "${versions[$i]}")" + rm -f "${versions[$i]}" + removed=$((removed + 1)) + done + fi + done + + if [[ $removed -gt 0 ]]; then + print_success "Removed $removed old package versions" + else + print_info "No old versions to remove" + fi +} + +# Note: We intentionally do NOT generate Packages.sig for local feeds +# opkg will skip signature verification if no .sig file exists +# This avoids "Failed to decode signature" errors for local/offline feeds +generate_packages_sig() { + local feed_dir="$1" + # Remove any existing .sig file to ensure opkg skips signature verification + rm -f "$feed_dir/Packages.sig" 2>/dev/null || true + print_info "Local feed configured without signature (opkg will skip verification)" +} + +# Strip libc dependency from IPK control file and repack +# This is needed because opkg reads the control file from inside the IPK +strip_libc_from_ipk() { + local ipk_file="$1" + # Get absolute path + ipk_file="$(cd "$(dirname "$ipk_file")" && pwd)/$(basename "$ipk_file")" + + local tmp_dir=$(mktemp -d) + local orig_dir=$(pwd) + + cd "$tmp_dir" || return 1 + + # Try tar format first (newer OpenWrt), then ar format (older) + if ! tar -xzf "$ipk_file" 2>/dev/null; then + if ! ar -x "$ipk_file" 2>/dev/null; then + cd "$orig_dir" + rm -rf "$tmp_dir" + return 1 + fi + fi + + # Extract and modify control file + local control_archive="" + if [[ -f control.tar.gz ]]; then + control_archive="control.tar.gz" + mkdir -p control_dir + tar -xzf control.tar.gz -C control_dir + elif [[ -f control.tar.zst ]]; then + control_archive="control.tar.zst" + mkdir -p control_dir + zstd -d control.tar.zst -o control.tar 2>/dev/null + tar -xf control.tar -C control_dir + rm -f control.tar + else + cd "$orig_dir" + rm -rf "$tmp_dir" + return 1 + fi + + # Strip libc from control file and remove blank lines + if [[ -f control_dir/control ]]; then + sed -i \ + -e 's/^Depends: libc$//' \ + -e 's/^Depends: libc, /Depends: /g' \ + -e 's/, libc$//g' \ + -e 's/, libc,/,/g' \ + control_dir/control + # Remove any blank lines (critical for opkg to parse correctly) + sed -i '/^$/d' control_dir/control + fi + + # Repack control archive + rm -f control.tar.gz control.tar.zst + tar -czf control.tar.gz -C control_dir . + rm -rf control_dir + + # Repack IPK (tar.gz format) - order matters: debian-binary, control, data + rm -f "$ipk_file" + tar -czf "$ipk_file" debian-binary control.tar.gz data.tar.* + + cd "$orig_dir" + rm -rf "$tmp_dir" + return 0 +} + +# Embed built packages into secubox-app-bonus as local feed +embed_local_feed() { + print_header "Embedding Local Package Feed" + + local feed_dir="$SCRIPT_DIR/../package/secubox/secubox-app-bonus/root/www/secubox-feed" + local pkg_ext="${PKG_EXT:-ipk}" + local src_dir="$BUILD_DIR/$ARCH" + + # Clean and create feed directory + rm -rf "$feed_dir" + mkdir -p "$feed_dir" + + # Check if we have packages to embed + if [[ ! -d "$src_dir" ]] || [[ -z "$(ls -A "$src_dir"/*.${pkg_ext} 2>/dev/null)" ]]; then + print_warning "No packages found to embed in local feed" + return 0 + fi + + # Copy all built packages EXCEPT secubox-app-bonus itself (avoid recursive inclusion) + print_info "Copying packages to local feed..." + for pkg in "$src_dir"/*.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + local basename=$(basename "$pkg") + # Skip secubox-app-bonus to avoid recursive inclusion (package including itself) + if [[ "$basename" =~ ^secubox-app-bonus_ ]]; then + print_info "Skipping $basename (avoid recursive inclusion)" + continue + fi + cp "$pkg" "$feed_dir/" + done + + # Clean old versions, keep only latest + clean_old_ipk_versions "$feed_dir" "$pkg_ext" + + # Strip libc from all IPK control files + print_info "Stripping libc from IPK control files..." + local stripped=0 + for pkg in "$feed_dir"/*.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + if strip_libc_from_ipk "$pkg"; then + stripped=$((stripped + 1)) + fi + done + print_success "Processed $stripped packages" + + local pkg_count=$(ls -1 "$feed_dir"/*.${pkg_ext} 2>/dev/null | wc -l) + print_info "Final package count: $pkg_count" + + # Generate Packages index for opkg + print_info "Generating Packages index..." + rm -f "$feed_dir/Packages" "$feed_dir/Packages.gz" + + for pkg in "$feed_dir"/*.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + local pkg_basename=$(basename "$pkg") + + # Extract control file from package (IPK is tar.gz containing control.tar.gz) + local control="" + if [[ "$pkg_ext" == "ipk" ]]; then + control=$(tar -xOzf "$pkg" control.tar.gz 2>/dev/null | tar -xOzf - ./control 2>/dev/null || true) + fi + + if [[ -n "$control" ]]; then + # Strip Source/SourceName/SourceDateEpoch/URL fields (cause opkg parsing issues) + echo "$control" | grep -v "^Source:\|^SourceName:\|^SourceDateEpoch:\|^URL:" >> "$feed_dir/Packages" + echo "Filename: $pkg_basename" >> "$feed_dir/Packages" + echo "Size: $(stat -c%s "$pkg")" >> "$feed_dir/Packages" + echo "" >> "$feed_dir/Packages" + fi + done + + # Create compressed index + gzip -kf "$feed_dir/Packages" 2>/dev/null || true + print_info "Generated Packages with $(grep -c '^Package:' "$feed_dir/Packages" 2>/dev/null || echo 0) packages" + + # Strip libc dependency from all packages + # The SDK adds libc to all packages, but for local feeds without libc + # this causes opkg to fail with "incompatible architectures" error + print_info "Stripping libc dependencies from packages..." + sed -i \ + -e 's/^Depends: libc$/Depends:/g' \ + -e 's/^Depends: libc, /Depends: /g' \ + -e 's/, libc$//g' \ + -e 's/, libc,/,/g' \ + -e 's/^Depends:$/Depends:/g' \ + "$feed_dir/Packages" + + # Clean up any empty or malformed Depends lines + sed -i \ + -e 's/^Depends: ,/Depends: /g' \ + -e 's/, ,/, /g' \ + -e 's/,$//' \ + "$feed_dir/Packages" + + # Regenerate compressed index after modification + gzip -kf "$feed_dir/Packages" 2>/dev/null || true + + # Do NOT create Packages.sig - opkg will skip signature verification if absent + # Creating an empty or invalid sig causes opkg to discard the package list + rm -f "$feed_dir/Packages.sig" 2>/dev/null || true + print_info "Local feed configured without signature (opkg will skip verification)" + + # Generate apps-local.json for appstore UI + print_info "Generating local apps manifest..." + generate_local_apps_json "$feed_dir" + + print_success "Local feed embedded with $pkg_count packages" + echo " Feed location: /www/secubox-feed/" + echo " opkg config: src secubox file:///www/secubox-feed" + + return 0 +} + +# Generate apps-local.json from built packages +generate_local_apps_json() { + local feed_dir="$1" + local json_file="$feed_dir/apps-local.json" + local pkg_ext="${PKG_EXT:-ipk}" + + cat > "$json_file" << 'HEADER' +{ + "feed_url": "/secubox-feed", + "generated": "TIMESTAMP", + "packages": [ +HEADER + sed -i "s/TIMESTAMP/$(date -Iseconds)/" "$json_file" + + local first=true + for pkg in "$feed_dir"/*.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + local filename=$(basename "$pkg") + local name=$(echo "$filename" | sed 's/_[0-9].*$//') + local version=$(echo "$filename" | sed 's/^[^_]*_//; s/_[^_]*$//') + local size=$(stat -c%s "$pkg") + + # Determine category and description based on package name + local category="utility" + local description="" + local icon="" + local luci_app="" + + case "$name" in + luci-app-crowdsec*) + category="security"; icon="shield"; description="CrowdSec security monitoring";; + luci-app-mitmproxy*) + category="security"; icon="lock"; description="HTTPS proxy and traffic inspection";; + luci-app-bandwidth*) + category="network"; icon="activity"; description="Bandwidth monitoring and control";; + luci-app-traffic*) + category="network"; icon="filter"; description="Traffic shaping and QoS";; + luci-app-client*) + category="network"; icon="users"; description="Client management and monitoring";; + luci-app-network*) + category="network"; icon="wifi"; description="Network configuration";; + luci-app-wireguard*) + category="vpn"; icon="shield"; description="WireGuard VPN dashboard";; + luci-app-vhost*) + category="network"; icon="server"; description="Virtual host management";; + luci-app-secubox*) + category="system"; icon="box"; description="SecuBox system component";; + luci-app-zigbee*) + category="iot"; icon="radio"; description="Zigbee device management";; + luci-app-mqtt*) + category="iot"; icon="message-square"; description="MQTT bridge";; + luci-app-ndpid*) + category="security"; icon="eye"; description="Deep packet inspection";; + luci-app-netdata*) + category="monitoring"; icon="bar-chart-2"; description="System monitoring dashboard";; + luci-app-system*) + category="system"; icon="settings"; description="System management";; + luci-app-cdn*) + category="network"; icon="globe"; description="CDN caching";; + luci-app-ksm*) + category="system"; icon="cpu"; description="Kernel memory management";; + luci-app-media*) + category="media"; icon="film"; description="Media streaming";; + luci-app-magicmirror*) + category="iot"; icon="monitor"; description="Smart mirror display";; + luci-app-auth*) + category="security"; icon="key"; description="Authentication management";; + luci-theme-*) + category="theme"; icon="palette"; description="LuCI theme";; + secubox-app-*) + category="secubox"; icon="package"; description="SecuBox backend service";; + secubox-core*) + category="system"; icon="box"; description="SecuBox core components";; + *) + category="utility"; icon="package"; description="SecuBox package";; + esac + + # Check if this package has a corresponding luci-app + if [[ "$name" =~ ^secubox-app- ]]; then + local app_name="${name#secubox-app-}" + luci_app="luci-app-${app_name}" + fi + + if [[ "$first" == "true" ]]; then + first=false + else + echo "," >> "$json_file" + fi + + cat >> "$json_file" << ENTRY + { + "name": "$name", + "version": "$version", + "filename": "$filename", + "size": $size, + "category": "$category", + "icon": "$icon", + "description": "$description", + "installed": false, + "luci_app": $([ -n "$luci_app" ] && echo "\"$luci_app\"" || echo "null") + } +ENTRY + done + + cat >> "$json_file" << 'FOOTER' + + ] +} +FOOTER + + print_success "Generated apps-local.json" +} + +# Run validation +run_validation() { + print_header "Running Validation" + + local failed=0 + + validate_makefiles || failed=$((failed + 1)) + validate_json || failed=$((failed + 1)) + validate_javascript || failed=$((failed + 1)) + validate_shellscripts || failed=$((failed + 1)) + check_file_permissions || failed=$((failed + 1)) + + if [[ $failed -gt 0 ]]; then + print_error "Validation failed with $failed error(s)" + return 1 + fi + + print_success "All validations passed!" + return 0 +} + +# Run build using OpenWrt buildroot (for packages that need system libraries like netifyd) +run_build_openwrt() { + local single_package="$1" + + print_header "Building $single_package with OpenWrt Buildroot" + print_info "This package requires system libraries not available in SDK" + echo "" + + check_dependencies + download_openwrt_source || return 1 + setup_openwrt_feeds || return 1 + copy_secubox_to_openwrt || return 1 + + cd "$OPENWRT_DIR" + + # Map shorthand names to actual directory names in package/secubox/ + declare -A DIR_NAME_MAP=( + ["nodogsplash"]="secubox-app-nodogsplash" + ["ndpid"]="secubox-app-ndpid" + ["netifyd"]="secubox-app-netifyd" + ["crowdsec"]="secubox-app-crowdsec" + ["mitmproxy"]="secubox-app-mitmproxy" + ["metablogizer"]="secubox-app-metablogizer" + ["tor"]="secubox-app-tor" + ["luci-app-tor"]="luci-app-tor" + ["cs-firewall-bouncer"]="secubox-app-cs-firewall-bouncer" + ["crowdsec-firewall-bouncer"]="secubox-app-cs-firewall-bouncer" + ) + + # Map directory names to actual package names (PKG_NAME in Makefile) + # Only needed when directory name differs from PKG_NAME + declare -A PKG_NAME_MAP=( + ["secubox-app-ndpid"]="ndpid" + ["secubox-app-netifyd"]="secubox-netifyd" + ["secubox-app-crowdsec"]="secubox-crowdsec" + ["secubox-app-nodogsplash"]="secubox-app-nodogsplash" + ["secubox-app-mitmproxy"]="secubox-app-mitmproxy" + ["secubox-app-metablogizer"]="secubox-app-metablogizer" + ["secubox-app-tor"]="secubox-app-tor" + ["secubox-app-cs-firewall-bouncer"]="secubox-app-cs-firewall-bouncer" + ["luci-app-tor"]="luci-app-tor" + ) + + # Resolve directory name (handle shorthand like "nodogsplash" -> "secubox-app-nodogsplash") + local dir_name="${DIR_NAME_MAP[$single_package]:-$single_package}" + + # Get actual package name (for config and finding .ipk) + local pkg_name="${PKG_NAME_MAP[$dir_name]:-$dir_name}" + print_info "Input: $single_package -> Directory: $dir_name -> Package: $pkg_name" + + # Update feeds + print_header "Installing Package from Feeds" + ./scripts/feeds update -a + + # For netifyd, remove old version from packages feed first + if [[ "$single_package" == "netifyd" ]]; then + ./scripts/feeds uninstall netifyd 2>/dev/null || true + fi + + # For Go packages (crowdsec, etc.), install golang build infrastructure first + if [[ "$dir_name" =~ ^(crowdsec|secubox-app-crowdsec)$ ]] || \ + grep -q "golang-package.mk" "../package/secubox/$dir_name/Makefile" 2>/dev/null; then + print_info "Installing Go language support for $dir_name..." + ./scripts/feeds install -a golang + fi + + # Force install from secubox feed (override any conflicts) + ./scripts/feeds install -f -p secubox "$dir_name" + + # Verify package is installed + if [[ ! -d "package/feeds/secubox/$dir_name" ]]; then + print_error "Package $dir_name not found in feeds after install" + print_info "Check if package exists in package/secubox/$dir_name" + cd - > /dev/null + return 1 + fi + + # Configure build for target architecture (mochabin = mvebu/cortexa72) + print_header "Configuring Build" + + # Set target configuration based on ARCH + case "$ARCH" in + aarch64_cortex-a72|aarch64-cortex-a72) + echo "CONFIG_TARGET_mvebu=y" >> .config + echo "CONFIG_TARGET_mvebu_cortexa72=y" >> .config + ;; + aarch64_cortex-a53|aarch64-cortex-a53) + echo "CONFIG_TARGET_mvebu=y" >> .config + echo "CONFIG_TARGET_mvebu_cortexa53=y" >> .config + ;; + x86-64|x86_64) + echo "CONFIG_TARGET_x86=y" >> .config + echo "CONFIG_TARGET_x86_64=y" >> .config + ;; + esac + + # Enable the package (use actual package name, not directory name) + echo "CONFIG_PACKAGE_${pkg_name}=m" >> .config + make defconfig FORCE=1 + + # Build dependencies first (for packages like netifyd that need system libraries) + print_header "Building Dependencies" + print_info "Downloading and building required system libraries..." + make package/libs/toolchain/compile V=s 2>&1 | grep -v "^make\[" || true + + # Build package + print_header "Building Package: $dir_name ($pkg_name)" + print_info "This may take several minutes on first build..." + echo "" + + # Build from SecuBox feed (via feeds system) + if make package/feeds/secubox/"$dir_name"/compile V=s; then + print_success "Package built successfully" + + # Find and display built package (search by actual package name) + local pkg_file=$(find bin/packages bin/targets -name "${pkg_name}*.ipk" -o -name "${pkg_name}_*.ipk" 2>/dev/null | head -1) + if [[ -z "$pkg_file" ]]; then + # Try alternative search patterns + pkg_file=$(find bin -name "*${pkg_name}*.ipk" 2>/dev/null | head -1) + fi + + if [[ -n "$pkg_file" ]]; then + echo "" + echo "📦 Built package:" + ls -lh "$pkg_file" + echo "" + + # Copy to build directory + mkdir -p "$BUILD_DIR/$ARCH" + cp "$pkg_file" "$BUILD_DIR/$ARCH/" + + print_success "Package copied to: $BUILD_DIR/$ARCH/" + else + print_warning "Package file not found in bin/, checking build directory..." + # The package might be in targets instead of packages + pkg_file=$(find bin/targets -name "${pkg_name}*.ipk" 2>/dev/null | head -1) + if [[ -n "$pkg_file" ]]; then + echo "📦 Built package:" + ls -lh "$pkg_file" + mkdir -p "$BUILD_DIR/$ARCH" + cp "$pkg_file" "$BUILD_DIR/$ARCH/" + print_success "Package copied to: $BUILD_DIR/$ARCH/" + fi + fi + else + print_error "Package build failed" + return 1 + fi + + cd - > /dev/null + print_info "Syncing OpenWrt packages into firmware tree..." + ARCH_NAME="$ARCH_NAME" "$REPO_ROOT/secubox-tools/sync-openwrt-packages.sh" || print_warning "Package sync script failed" + return 0 +} + +# Rebuild secubox-app-bonus with populated local feed +rebuild_bonus_package() { + print_header "Rebuilding secubox-app-bonus with Local Feed" + + local pkg_ext="${PKG_EXT:-ipk}" + local bonus_pkg="secubox-app-bonus" + + # Sync the updated secubox-app-bonus to local-feed + print_info "Syncing secubox-app-bonus to local-feed..." + local src_dir="$SCRIPT_DIR/../package/secubox/$bonus_pkg" + local feed_dir="$SDK_DIR/../local-feed/$bonus_pkg" + + if [[ ! -d "$src_dir" ]]; then + print_error "Source directory not found: $src_dir" + return 1 + fi + + rsync -av --delete "$src_dir/" "$feed_dir/" + print_success "Synced to local-feed" + + # Update the feed and install the package + cd "$SDK_DIR" + + print_info "Updating feeds..." + ./scripts/feeds update secubox + ./scripts/feeds install "$bonus_pkg" 2>&1 | grep -v "WARNING:" || true + + # Enable and rebuild the package + echo "CONFIG_PACKAGE_${bonus_pkg}=m" >> .config + make defconfig FORCE=1 2>/dev/null + + print_info "Building $bonus_pkg..." + local build_log="/tmp/build-${bonus_pkg}.log" + + if timeout 600 make "package/feeds/secubox/${bonus_pkg}/compile" V=s -j1 NO_DEPS=1 FORCE=1 > "$build_log" 2>&1; then + local pkg_file=$(find bin -name "${bonus_pkg}*.${pkg_ext}" 2>/dev/null | head -1) + + if [[ -n "$pkg_file" ]]; then + print_success "Built: $bonus_pkg" + echo " → $pkg_file" + + # Copy to build directory (but NOT into the feed - avoid recursive inclusion) + mkdir -p "$BUILD_DIR/$ARCH" + cp "$pkg_file" "$BUILD_DIR/$ARCH/" + + # NOTE: We do NOT copy secubox-app-bonus into its own feed directory + # This would cause infinite size growth (package including itself) + + # Regenerate Packages index (without secubox-app-bonus) + print_info "Regenerating Packages index..." + ( + cd "$feed_src_dir" + rm -f Packages Packages.gz + + for pkg in *.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + + local control="" + if [[ "$pkg_ext" == "ipk" ]]; then + control=$(tar -xzOf "$pkg" ./control.tar.gz 2>/dev/null | tar -xzOf - ./control 2>/dev/null || \ + ar -p "$pkg" control.tar.gz 2>/dev/null | tar -xzOf - ./control 2>/dev/null || \ + ar -p "$pkg" control.tar.zst 2>/dev/null | zstd -d 2>/dev/null | tar -xOf - ./control 2>/dev/null || true) + fi + + if [[ -n "$control" ]]; then + echo "$control" + echo "Filename: $pkg" + echo "Size: $(stat -c%s "$pkg")" + echo "" + fi + done > Packages + + # Strip libc dependencies + sed -i \ + -e 's/^Depends: libc$/Depends:/g' \ + -e 's/^Depends: libc, /Depends: /g' \ + -e 's/, libc$//g' \ + -e 's/, libc,/,/g' \ + Packages + + gzip -kf Packages 2>/dev/null || true + rm -f Packages.sig 2>/dev/null || true + ) + print_success "Packages index regenerated" + else + print_warning "No .${pkg_ext} generated for $bonus_pkg" + tail -50 "$build_log" + return 1 + fi + else + print_error "Build failed: $bonus_pkg" + tail -100 "$build_log" + return 1 + fi + + cd - > /dev/null + print_success "secubox-app-bonus rebuilt with local feed" + return 0 +} + +# Run build +run_build() { + local single_package="$1" + + # Packages that are OpenWrt buildroot only + if [[ -n "$single_package" ]] && is_openwrt_only_pkg "$single_package"; then + run_build_openwrt "$single_package" + return $? + fi + + check_dependencies + download_sdk || return 1 + setup_sdk_feeds || return 1 + copy_packages "$single_package" || return 1 + configure_packages "$single_package" || return 1 + build_packages "$single_package" || return 1 + collect_artifacts || return 1 + embed_local_feed || return 1 + rebuild_bonus_package || return 1 + + print_header "Build Complete!" + print_success "Packages available in: $BUILD_DIR/$ARCH/" + print_info "Local feed embedded in secubox-app-bonus with Packages index" + + return 0 +} + +# ============================================ +# Firmware Image Building Functions +# ============================================ + +# Parse device profile +parse_device_profile() { + local device="$1" + + if [[ -z "${DEVICE_PROFILES[$device]}" ]]; then + print_error "Unknown device: $device" + print_info "Available devices: ${!DEVICE_PROFILES[*]}" + return 1 + fi + + local profile="${DEVICE_PROFILES[$device]}" + IFS=':' read -r TARGET SUBTARGET PROFILE_NAME DESCRIPTION <<< "$profile" + + export FW_TARGET="$TARGET" + export FW_SUBTARGET="$SUBTARGET" + export FW_PROFILE="$PROFILE_NAME" + export FW_DESCRIPTION="$DESCRIPTION" + export FW_DEVICE="$device" + + return 0 +} + +# Download OpenWrt source +download_openwrt_source() { + print_header "Downloading OpenWrt Source" + + if [[ -d "$OPENWRT_DIR/.git" ]]; then + print_info "OpenWrt source already exists, checking version..." + cd "$OPENWRT_DIR" + local current_version=$(git describe --tags 2>/dev/null || echo "unknown") + if [[ "$current_version" == "v${OPENWRT_VERSION}" ]]; then + print_success "Using existing OpenWrt $OPENWRT_VERSION" + cd - > /dev/null + return 0 + else + print_info "Version mismatch (current: $current_version), re-cloning..." + cd - > /dev/null + rm -rf "$OPENWRT_DIR" + fi + fi + + print_info "Cloning OpenWrt $OPENWRT_VERSION..." + + if [[ "$OPENWRT_VERSION" == "SNAPSHOT" ]]; then + git clone --depth 1 https://github.com/openwrt/openwrt.git "$OPENWRT_DIR" + else + git clone --depth 1 --branch "v${OPENWRT_VERSION}" \ + https://github.com/openwrt/openwrt.git "$OPENWRT_DIR" + fi + + print_success "OpenWrt source downloaded" + return 0 +} + +# Setup OpenWrt feeds for firmware build +setup_openwrt_feeds() { + print_header "Setting up OpenWrt Feeds" + + cd "$OPENWRT_DIR" + + # Fix: Create rsync symlink in staging_dir for OpenWrt build environment + # OpenWrt uses a restricted PATH and doesn't see system rsync + if command -v rsync &>/dev/null; then + mkdir -p staging_dir/host/bin + if [[ ! -L staging_dir/host/bin/rsync ]]; then + ln -sf "$(command -v rsync)" staging_dir/host/bin/rsync + print_success "Created rsync symlink in staging_dir/host/bin/" + fi + else + print_warning "rsync not found on system - some builds may fail" + fi + + # Remove unwanted feeds + if [[ -f "feeds.conf.default" ]]; then + sed -i '/telephony/d' feeds.conf.default + sed -i '/routing/d' feeds.conf.default + print_success "Removed telephony and routing from feeds.conf.default" + fi + + # Update feeds + print_info "Updating feeds (this may take a few minutes)..." + if ! ./scripts/feeds update -a 2>&1 | tee feed-update.log; then + print_warning "Feed update had errors, continuing..." + fi + + # Install feeds + print_info "Installing feeds..." + if ! ./scripts/feeds install -a 2>&1 | tee feed-install.log; then + print_warning "Feed install had warnings, checking directories..." + fi + + # Note: Skipping Lua header installation + # Our packages are PKGARCH:=all (scripts only) - no compilation needed + + # Verify feeds + for feed in packages luci; do + if [[ -d "feeds/$feed" ]]; then + local feed_size=$(du -sh "feeds/$feed" 2>/dev/null | cut -f1) + print_success "feeds/$feed ($feed_size)" + else + print_error "feeds/$feed missing!" + cd - > /dev/null + return 1 + fi + done + + cd - > /dev/null + print_success "OpenWrt feeds configured" + return 0 +} + +# Copy SecuBox packages to OpenWrt +copy_secubox_to_openwrt() { + print_header "Copying SecuBox Packages to OpenWrt" + + cd "$OPENWRT_DIR" + + mkdir -p package/secubox + + local pkg_count=0 + + # Copy luci-app-* packages + for pkg in ../../luci-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " ✅ $pkg_name" + cp -r "$pkg" package/secubox/ + + # Fix Makefile include path + if [[ -f "package/secubox/$pkg_name/Makefile" ]]; then + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' \ + "package/secubox/$pkg_name/Makefile" + fi + + pkg_count=$((pkg_count + 1)) + fi + done + + # Copy luci-theme-* packages + for pkg in ../../luci-theme-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " ✅ $pkg_name" + cp -r "$pkg" package/secubox/ + + # Fix Makefile include path + if [[ -f "package/secubox/$pkg_name/Makefile" ]]; then + sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' \ + "package/secubox/$pkg_name/Makefile" + fi + + pkg_count=$((pkg_count + 1)) + fi + done + + # Copy secubox-app-* helper packages + for pkg in ../../package/secubox/secubox-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " ✅ $pkg_name" + cp -r "$pkg" package/secubox/ + pkg_count=$((pkg_count + 1)) + fi + done + + # Copy additional core packages (non-LuCI / non-app store) + for pkg in ../../package/secubox/*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " ✅ $pkg_name" + cp -r "$pkg" package/secubox/ + pkg_count=$((pkg_count + 1)) + fi + done + + rm -f package/secubox/luci-app-secubox-netifyd/root/etc/config/netifyd >/dev/null 2>&1 || true + + cd - > /dev/null + + print_success "Copied $pkg_count SecuBox packages" + return 0 +} + +# Generate firmware configuration +generate_firmware_config() { + print_header "Generating Firmware Configuration" + + cd "$OPENWRT_DIR" + + print_info "Device: $FW_DESCRIPTION" + print_info "Target: $FW_TARGET/$FW_SUBTARGET" + print_info "Profile: $FW_PROFILE" + + # Base configuration + cat > .config << EOF +# Target +CONFIG_TARGET_${FW_TARGET}=y +CONFIG_TARGET_${FW_TARGET}_${FW_SUBTARGET}=y +CONFIG_TARGET_${FW_TARGET}_${FW_SUBTARGET}_DEVICE_${FW_PROFILE}=y + +# Image building (REQUIRED for firmware generation) +CONFIG_TARGET_MULTI_PROFILE=n +CONFIG_TARGET_ALL_PROFILES=n +CONFIG_TARGET_PER_DEVICE_ROOTFS=y + +# Image settings +CONFIG_TARGET_ROOTFS_SQUASHFS=y +CONFIG_TARGET_ROOTFS_EXT4FS=y +CONFIG_TARGET_KERNEL_PARTSIZE=32 +CONFIG_TARGET_ROOTFS_PARTSIZE=16384 + +# Disable GDB in toolchain (fixes build issues) +# CONFIG_GDB is not set +CONFIG_BUILD_LOG=y + +# Package conflict resolution +# CONFIG_PACKAGE_lucihttp is not set (fails in SDK) +# CONFIG_PACKAGE_cgi-io is not set (fails in SDK) +CONFIG_AUTOREMOVE=y + +# Base packages +CONFIG_PACKAGE_luci=y +CONFIG_PACKAGE_luci-ssl=y +CONFIG_PACKAGE_luci-app-opkg=y +CONFIG_PACKAGE_luci-theme-openwrt-2020=y +CONFIG_PACKAGE_luci-theme-secubox=y + +# DNS Server (fix conflict: use dnsmasq-full only) +# CONFIG_PACKAGE_dnsmasq is not set +CONFIG_PACKAGE_dnsmasq-full=y + +# Networking essentials +CONFIG_PACKAGE_curl=y +CONFIG_PACKAGE_wget-ssl=y +CONFIG_PACKAGE_iptables=y +CONFIG_PACKAGE_ip6tables=y + +# USB support +CONFIG_PACKAGE_kmod-usb-core=y +CONFIG_PACKAGE_kmod-usb3=y +CONFIG_PACKAGE_kmod-usb-storage=y + +# Filesystem +CONFIG_PACKAGE_kmod-fs-ext4=y +CONFIG_PACKAGE_kmod-fs-vfat=y + +# Container/LXC support +CONFIG_PACKAGE_kmod-veth=y +CONFIG_PACKAGE_kmod-br-netfilter=y +CONFIG_PACKAGE_kmod-nf-conntrack-netlink=y + +# SecuBox packages - Core +CONFIG_PACKAGE_secubox-app=y +CONFIG_PACKAGE_luci-app-secubox=y +CONFIG_PACKAGE_luci-app-system-hub=y +CONFIG_PACKAGE_luci-app-secubox-admin=y + +# SecuBox packages - Security & Monitoring +# CONFIG_PACKAGE_luci-app-crowdsec-dashboard is not set (requires crowdsec backend - compile fails) +CONFIG_PACKAGE_luci-app-netdata-dashboard=y +CONFIG_PACKAGE_crowdsec=y +CONFIG_PACKAGE_secubox-app-crowdsec=y + +# SecuBox packages - Network Intelligence +CONFIG_PACKAGE_netifyd=y +CONFIG_PACKAGE_luci-app-secubox-netifyd=y +CONFIG_PACKAGE_luci-app-network-modes=y + +# SecuBox packages - VPN & Access Control +CONFIG_PACKAGE_luci-app-wireguard-dashboard=y +CONFIG_PACKAGE_luci-app-client-guardian=y +# CONFIG_PACKAGE_luci-app-auth-guardian is not set (not stable yet) + +# SecuBox packages - Bandwidth & Traffic +CONFIG_PACKAGE_luci-app-bandwidth-manager=y +CONFIG_PACKAGE_luci-app-media-flow=y + +# SecuBox packages - Performance & Services +CONFIG_PACKAGE_luci-app-cdn-cache=y +CONFIG_PACKAGE_luci-app-vhost-manager=y + +# SecuBox packages - Disabled (require compilation/not ready) +# CONFIG_PACKAGE_luci-app-ksm-manager is not set (not stable) +# CONFIG_PACKAGE_luci-app-traffic-shaper is not set (not stable) + +# WireGuard +CONFIG_PACKAGE_wireguard-tools=y +CONFIG_PACKAGE_kmod-wireguard=y +CONFIG_PACKAGE_qrencode=y +EOF + + # Device-specific packages + case "$FW_DEVICE" in + mochabin) + cat >> .config << EOF + +# MOCHAbin specific - 10G networking +CONFIG_PACKAGE_kmod-sfp=y +CONFIG_PACKAGE_kmod-phy-marvell-10g=y +EOF + ;; + espressobin-ultra) + cat >> .config << EOF + +# WiFi support +CONFIG_PACKAGE_kmod-mt76=y +CONFIG_PACKAGE_kmod-mac80211=y +EOF + ;; + esac + + # Run defconfig + make defconfig FORCE=1 2>/dev/null + + cd - > /dev/null + + print_success "Configuration generated" + return 0 +} + +# Verify firmware configuration +verify_firmware_config() { + print_header "Verifying Firmware Configuration" + + cd "$OPENWRT_DIR" + + # Check device profile + if grep -q "CONFIG_TARGET_${FW_TARGET}_${FW_SUBTARGET}_DEVICE_${FW_PROFILE}=y" .config; then + print_success "Device profile correctly configured" + else + print_error "Device profile not found in .config!" + print_info "Searching for available profiles..." + find "target/$FW_TARGET/$FW_SUBTARGET" -name "*.mk" -exec grep -l "DEVICE_NAME" {} \; 2>/dev/null | head -5 + cd - > /dev/null + return 1 + fi + + # Check image generation + if grep -q "CONFIG_TARGET_ROOTFS_SQUASHFS=y" .config; then + print_success "SQUASHFS image generation enabled" + fi + + if grep -q "CONFIG_TARGET_ROOTFS_EXT4FS=y" .config; then + print_success "EXT4 image generation enabled" + fi + + # Show relevant config + echo "" + print_info "Device configuration:" + grep "^CONFIG_TARGET_" .config | head -10 + + cd - > /dev/null + + print_success "Configuration verified" + return 0 +} + +# Build firmware image +build_firmware_image() { + print_header "Building Firmware Image" + + cd "$OPENWRT_DIR" + + print_info "Device: $FW_DESCRIPTION" + print_info "Target: $FW_TARGET/$FW_SUBTARGET" + print_info "Profile: $FW_PROFILE" + print_info "CPU Cores: $(nproc)" + echo "" + + local start_time=$(date +%s) + + # Download packages first + print_info "Downloading packages..." + if ! make download -j$(nproc) V=s; then + print_warning "Parallel download failed, retrying single-threaded..." + make download -j1 V=s + fi + + echo "" + print_header "Compiling Firmware (This may take 1-2 hours)" + echo "" + + # Create necessary directories to avoid opkg lock file errors + # Find all root directories and ensure tmp subdirectories exist + find build_dir -type d -name "root.orig-*" -exec mkdir -p {}/tmp \; 2>/dev/null || true + find build_dir -type d -name "root-*" -exec mkdir -p {}/tmp \; 2>/dev/null || true + + # Build with explicit PROFILE + if make -j$(nproc) PROFILE="$FW_PROFILE" V=s 2>&1 | tee build.log; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + local minutes=$((duration / 60)) + local seconds=$((duration % 60)) + + print_success "Build completed in ${minutes}m ${seconds}s" + else + print_error "Parallel build failed, retrying single-threaded..." + + # Ensure staging directories exist before retry + find build_dir -type d -name "root.orig-*" -exec mkdir -p {}/tmp \; 2>/dev/null || true + find build_dir -type d -name "root-*" -exec mkdir -p {}/tmp \; 2>/dev/null || true + + if make -j1 PROFILE="$FW_PROFILE" V=s 2>&1 | tee build-retry.log; then + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + local minutes=$((duration / 60)) + local seconds=$((duration % 60)) + + print_success "Build completed in ${minutes}m ${seconds}s (after retry)" + else + print_error "Build failed!" + echo "" + echo "Last 50 lines of build log:" + tail -50 build-retry.log + cd - > /dev/null + return 1 + fi + fi + + cd - > /dev/null + return 0 +} + +# Collect firmware artifacts +collect_firmware_artifacts() { + print_header "Collecting Firmware Artifacts" + + local target_dir="$OPENWRT_DIR/bin/targets/$FW_TARGET/$FW_SUBTARGET" + local output_dir="$BUILD_DIR/firmware/$FW_DEVICE" + + mkdir -p "$output_dir" + + # Find and copy firmware images + local img_count=0 + if [[ -d "$target_dir" ]]; then + echo "🔍 Target directory: $target_dir" + echo "" + echo "📂 All files in target directory:" + ls -lh "$target_dir" 2>/dev/null | grep -v "^total" || echo " (empty)" + echo "" + + echo "📦 Copying firmware images..." + while IFS= read -r file; do + case "$(basename "$file")" in + *.ipk|*.manifest|*.json|sha256sums|*.buildinfo|packages) + continue + ;; + *) + cp "$file" "$output_dir/" + print_success "$(basename "$file") ($(du -h "$file" | cut -f1))" + img_count=$((img_count + 1)) + ;; + esac + done < <(find "$target_dir" -maxdepth 1 -type f 2>/dev/null) + else + print_error "Target directory not found: $target_dir" + fi + + if [[ $img_count -eq 0 ]]; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_warning "No firmware images found!" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + # Diagnostic information + echo "🔍 Diagnostic Information:" + echo "" + + if [[ -d "$OPENWRT_DIR/bin/targets" ]]; then + echo "📂 Available targets:" + find "$OPENWRT_DIR/bin/targets" -type d -mindepth 2 -maxdepth 2 2>/dev/null | sed 's|.*/bin/targets/||' || echo " (none)" + echo "" + fi + + if [[ -f "$OPENWRT_DIR/build.log" ]]; then + echo "📋 Checking build log for errors..." + if grep -i "error\|failed\|cannot" "$OPENWRT_DIR/build.log" | tail -10 | grep -v "warning" > /tmp/fw-errors.txt 2>/dev/null && [[ -s /tmp/fw-errors.txt ]]; then + echo "Recent errors found:" + cat /tmp/fw-errors.txt + rm -f /tmp/fw-errors.txt + else + echo " No obvious errors in build log" + fi + echo "" + fi + + if [[ -d "$target_dir" ]]; then + local all_files=$(find "$target_dir" -type f 2>/dev/null | wc -l) + echo "🎯 Target directory analysis:" + echo " Total files: $all_files" + if [[ $all_files -gt 0 ]]; then + echo " File types:" + find "$target_dir" -type f 2>/dev/null -exec basename {} \; | sed 's/.*\./ ./' | sort -u + fi + fi + + echo "" + print_warning "This usually means:" + echo " 1. Device profile was not properly selected" + echo " 2. Build completed but only packages were built, not images" + echo " 3. Device profile name doesn't match OpenWrt $OPENWRT_VERSION" + echo "" + print_info "To debug:" + echo " 1. Check: $OPENWRT_DIR/.config for CONFIG_TARGET settings" + echo " 2. Review: $OPENWRT_DIR/build.log for errors" + echo " 3. Verify profile exists: find $OPENWRT_DIR/target/$FW_TARGET/$FW_SUBTARGET -name '*.mk'" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + fi + + # Copy packages + mkdir -p "$output_dir/packages" + find "$OPENWRT_DIR/bin/packages" -name "luci-app-*.ipk" -exec cp {} "$output_dir/packages/" \; 2>/dev/null || true + find "$OPENWRT_DIR/bin/packages" -name "*secubox*.ipk" -exec cp {} "$output_dir/packages/" \; 2>/dev/null || true + find "$OPENWRT_DIR/bin/packages" -name "netifyd*.ipk" -exec cp {} "$output_dir/packages/" \; 2>/dev/null || true + + local pkg_count=$(find "$output_dir/packages" -name "*.ipk" 2>/dev/null | wc -l) + + # Generate checksums + cd "$output_dir" + sha256sum *.* > SHA256SUMS 2>/dev/null || true + if [[ -d packages && -n "$(ls -A packages 2>/dev/null)" ]]; then + (cd packages && sha256sum *.ipk > SHA256SUMS 2>/dev/null || true) + fi + cd - > /dev/null + + # Create build info + cat > "$output_dir/BUILD_INFO.txt" << EOF +SecuBox Firmware Build +====================== +Device: $FW_DESCRIPTION +Profile: $FW_PROFILE +Target: $FW_TARGET/$FW_SUBTARGET +OpenWrt: $OPENWRT_VERSION +Built: $(date -u +%Y-%m-%dT%H:%M:%SZ) + +Firmware Images: $img_count +SecuBox Packages: $pkg_count +EOF + + echo "" + print_success "Firmware images: $img_count" + print_success "SecuBox packages: $pkg_count" + print_success "Artifacts saved to: $output_dir" + + echo "" + print_info "Contents:" + ls -lh "$output_dir" + + return 0 +} + +# Debug firmware configuration +debug_firmware_build() { + local device="$1" + + if [[ -z "$device" ]]; then + print_error "Device not specified" + print_info "Usage: $0 debug-firmware " + print_info "Available devices: ${!DEVICE_PROFILES[*]}" + return 1 + fi + + # Parse device profile + parse_device_profile "$device" || return 1 + + print_header "Firmware Build Debug Information" + + echo "Device Configuration:" + echo " Device: $FW_DEVICE" + echo " Description: $FW_DESCRIPTION" + echo " Target: $FW_TARGET" + echo " Subtarget: $FW_SUBTARGET" + echo " Profile: $FW_PROFILE" + echo "" + + if [[ -d "$OPENWRT_DIR" ]]; then + print_info "OpenWrt source exists at: $OPENWRT_DIR" + + if [[ -f "$OPENWRT_DIR/.config" ]]; then + echo "" + echo "Current .config settings:" + grep "^CONFIG_TARGET_" "$OPENWRT_DIR/.config" | head -20 + + echo "" + echo "Checking device profile..." + if grep -q "CONFIG_TARGET_${FW_TARGET}_${FW_SUBTARGET}_DEVICE_${FW_PROFILE}=y" "$OPENWRT_DIR/.config"; then + print_success "Device profile is configured" + else + print_error "Device profile NOT configured!" + fi + + echo "" + echo "Available device profiles for $FW_TARGET/$FW_SUBTARGET:" + find "$OPENWRT_DIR/target/$FW_TARGET/$FW_SUBTARGET" -name "*.mk" 2>/dev/null | \ + xargs grep -l "DEVICE_NAME" 2>/dev/null | head -10 + else + print_warning "No .config file found - run build-firmware first" + fi + + echo "" + if [[ -d "$OPENWRT_DIR/bin/targets/$FW_TARGET/$FW_SUBTARGET" ]]; then + echo "Build output directory exists:" + echo " Path: $OPENWRT_DIR/bin/targets/$FW_TARGET/$FW_SUBTARGET" + echo " Files:" + ls -lh "$OPENWRT_DIR/bin/targets/$FW_TARGET/$FW_SUBTARGET" 2>/dev/null | grep -v "^total" | head -20 + else + print_warning "Build output directory doesn't exist yet" + fi + else + print_warning "OpenWrt source not downloaded yet" + print_info "Run: $0 build-firmware $device" + fi + + return 0 +} + +# Run firmware build +run_firmware_build() { + local device="$1" + + if [[ -z "$device" ]]; then + print_error "Device not specified" + print_info "Usage: $0 build-firmware " + print_info "Available devices: ${!DEVICE_PROFILES[*]}" + return 1 + fi + + # Parse device profile + parse_device_profile "$device" || return 1 + + # Check dependencies + check_dependencies + + # Build firmware + download_openwrt_source || return 1 + + # Fix: Ensure rsync is available in OpenWrt staging directory + # OpenWrt build uses restricted PATH and may not see system rsync + if command -v rsync &>/dev/null; then + mkdir -p "$OPENWRT_DIR/staging_dir/host/bin" + if [[ ! -e "$OPENWRT_DIR/staging_dir/host/bin/rsync" ]]; then + ln -sf "$(command -v rsync)" "$OPENWRT_DIR/staging_dir/host/bin/rsync" + print_success "Created rsync symlink in staging_dir" + fi + fi + + setup_openwrt_feeds || return 1 + copy_secubox_to_openwrt || return 1 + generate_firmware_config || return 1 + verify_firmware_config || return 1 + build_firmware_image || return 1 + collect_firmware_artifacts || return 1 + + print_header "Firmware Build Complete!" + print_success "Device: $FW_DESCRIPTION" + print_success "Location: $BUILD_DIR/firmware/$FW_DEVICE/" + + return 0 +} + +# Build native packages using full OpenWrt toolchain (without firmware) +run_toolchain_build() { + local device="${1:-espressobin-v7}" + local packages=("${@:2}") + + print_header "Building Native Packages (Toolchain)" + + # Parse device profile for architecture + parse_device_profile "$device" || return 1 + + # Check dependencies + check_dependencies + + # Setup OpenWrt environment + download_openwrt_source || return 1 + + # Fix: Ensure rsync is available in OpenWrt staging directory + if command -v rsync &>/dev/null; then + mkdir -p "$OPENWRT_DIR/staging_dir/host/bin" + if [[ ! -e "$OPENWRT_DIR/staging_dir/host/bin/rsync" ]]; then + ln -sf "$(command -v rsync)" "$OPENWRT_DIR/staging_dir/host/bin/rsync" + print_success "Created rsync symlink in staging_dir" + fi + fi + + setup_openwrt_feeds || return 1 + copy_secubox_to_openwrt || return 1 + + # Generate minimal config for package building + print_header "Generating Package Build Configuration" + cd "$OPENWRT_DIR" + + # Start with minimal config + cat > .config << EOF +# Target configuration for $FW_TARGET/$FW_SUBTARGET +CONFIG_TARGET_${FW_TARGET}=y +CONFIG_TARGET_${FW_TARGET}_${FW_SUBTARGET}=y +CONFIG_TARGET_MULTI_PROFILE=y +CONFIG_TARGET_ALL_PROFILES=y + +# Build packages only (no firmware) +CONFIG_ALL_NONSHARED=n +CONFIG_ALL_KMODS=n +CONFIG_ALL=n + +# Enable SecuBox native packages +CONFIG_PACKAGE_secubox-app-cs-firewall-bouncer=m +CONFIG_PACKAGE_secubox-app-crowdsec=m +CONFIG_PACKAGE_secubox-app-ndpid=m +CONFIG_PACKAGE_secubox-app-netifyd=m +CONFIG_PACKAGE_secubox-app-nodogsplash=m + +# Required dependencies +CONFIG_PACKAGE_nftables=y +CONFIG_PACKAGE_golang=y + +# Disable GDB to speed up build +CONFIG_GDB=n +EOF + + # Expand config + make defconfig + + # Download sources + print_info "Downloading package sources..." + make download -j$(nproc) V=s 2>&1 | grep -v "^make\[" || true + + # Build prerequisite targets first + # This builds tools, toolchain, and target preparation in one step + print_header "Building Prerequisites (Tools + Toolchain)" + print_info "This may take 1-2 hours on first run..." + + # Build tools, toolchain, and target preparation (needed for packages) + if ! make tools/install toolchain/install target/compile V=s -j$(nproc) 2>&1 | tee build-prereqs.log; then + print_warning "Prerequisites build had issues, continuing..." + fi + print_success "Prerequisites built" + + # Build Go compiler for host (needed for Go packages) + print_header "Building Go Compiler (for Go packages)" + if ! make package/feeds/packages/lang/golang/host/compile V=s -j$(nproc) 2>&1 | tee build-golang.log; then + print_warning "Go compiler build had issues, some packages may fail" + fi + print_success "Go compiler ready" + + # Build specific packages or all native SecuBox packages + print_header "Compiling Native Packages" + + local native_packages=( + "secubox-app-cs-firewall-bouncer" + "secubox-app-crowdsec" + "secubox-app-ndpid" + "secubox-app-netifyd" + "secubox-app-nodogsplash" + ) + + if [[ ${#packages[@]} -gt 0 ]]; then + native_packages=("${packages[@]}") + fi + + local built=() + local failed=() + + for pkg in "${native_packages[@]}"; do + print_info "Building: $pkg" + if make package/$pkg/compile V=s -j$(nproc) 2>&1 | tee -a build-$pkg.log; then + built+=("$pkg") + print_success "Built: $pkg" + else + failed+=("$pkg") + print_error "Failed: $pkg" + fi + done + + # Collect built packages + print_header "Collecting Built Packages" + + local output_dir="$BUILD_DIR/toolchain-$ARCH" + mkdir -p "$output_dir" + + # Find and copy IPK packages + find "$OPENWRT_DIR/bin/packages" -name "*.ipk" -exec cp {} "$output_dir/" \; 2>/dev/null || true + find "$OPENWRT_DIR/bin/targets" -name "*.ipk" -exec cp {} "$output_dir/" \; 2>/dev/null || true + + local pkg_count=$(find "$output_dir" -name "*.ipk" 2>/dev/null | wc -l) + + cd - > /dev/null + + # Summary + print_header "Toolchain Build Summary" + print_success "Built: ${#built[@]} packages: ${built[*]}" + if [[ ${#failed[@]} -gt 0 ]]; then + print_error "Failed: ${#failed[@]} packages: ${failed[*]}" + fi + print_success "Total IPK packages: $pkg_count" + print_success "Location: $output_dir/" + + return 0 +} + +# Show usage +show_usage() { + cat << EOF +SecuBox Local Build Tool +Replicates GitHub Actions workflows for local development + +USAGE: + $0 [options] + +COMMANDS: + validate Run validation only (lint, syntax checks) + build Build all packages for x86_64 + build Build single package + build --arch Build for specific architecture + build-toolchain Build native packages (Go/C++) using full toolchain + build-firmware Build full firmware image for device + debug-firmware Debug firmware build (check config without building) + full Run validation then build + clean Clean build directories + clean-all Clean all build directories including OpenWrt source and local-feed + sync Sync packages from package/secubox to local-feed + sync-feed Clean old IPKs and regenerate feed (Packages, Packages.sig, apps-local.json) + deploy [router] [packages] Deploy packages to router (default: 192.168.255.1) + help Show this help message + +PACKAGES: + SDK packages (scripts only, fast build): + luci-app-* LuCI application packages + luci-theme-* LuCI theme packages + + Toolchain packages (native code, requires full OpenWrt build): + ndpid nDPId DPI engine (shorthand for secubox-app-ndpid) + netifyd Netifyd DPI engine (shorthand for secubox-app-netifyd) + nodogsplash Captive portal (shorthand for secubox-app-nodogsplash) + crowdsec CrowdSec IPS (shorthand for secubox-app-crowdsec) + cs-firewall-bouncer CrowdSec Firewall Bouncer (Go binary) + mitmproxy mitmproxy HTTPS proxy (binary download) + metablogizer Metablogizer (Python dependencies) + tor Tor service (shorthand for secubox-app-tor) + secubox-app-* Full directory names also accepted + +ARCHITECTURES (for package building): + aarch64-cortex-a72 ARM Cortex-A72 (MOCHAbin, RPi4) (default) + aarch64-cortex-a53 ARM Cortex-A53 (ESPRESSObin) + x86-64 PC, VMs + aarch64-generic Generic ARM64 + mips-24kc MIPS 24Kc (TP-Link) + mipsel-24kc MIPS LE (Xiaomi, GL.iNet) + +DEVICES (for firmware building): + espressobin-v7 ESPRESSObin V7 (1-2GB DDR4) + espressobin-ultra ESPRESSObin Ultra (PoE, WiFi) + mochabin MOCHAbin (Quad-core A72, 10G) + x86-64 x86_64 Generic PC + +EXAMPLES: + # Validate all packages + $0 validate + + # Build all SDK packages for default architecture (mochabin) + $0 build + + # Build single LuCI package (SDK - fast) + $0 build luci-app-system-hub + + # Build nDPId DPI engine (toolchain - native code) + $0 build ndpid + + # Build Netifyd DPI engine (toolchain) + $0 build netifyd + + # Build Nodogsplash captive portal (toolchain) + $0 build nodogsplash + + # Build CrowdSec IPS (toolchain - Go) + $0 build crowdsec + + # Build mitmproxy HTTPS proxy (toolchain - binary download) + $0 build mitmproxy + + # Build CrowdSec Firewall Bouncer (toolchain - Go) + $0 build cs-firewall-bouncer + + # Build Tor service (toolchain) + $0 build tor + + # Build Metablogizer (toolchain - Python deps) + $0 build metablogizer + + # Build using full directory name + $0 build secubox-app-ndpid + + # Build netifyd LuCI app (SDK - scripts only) + $0 build luci-app-secubox-netifyd + + # Build for specific architecture + $0 build ndpid --arch x86-64 + + # Build firmware image for MOCHAbin + $0 build-firmware mochabin + + # Build firmware image for ESPRESSObin V7 + $0 build-firmware espressobin-v7 + + # Debug firmware build configuration + $0 debug-firmware mochabin + + # Full validation and build + $0 full + + # Clean build artifacts + $0 clean + + # Clean everything including OpenWrt source + $0 clean-all + +ENVIRONMENT VARIABLES: +OPENWRT_VERSION OpenWrt version (default: 24.10.5) + SDK_DIR SDK directory (default: ./sdk) + BUILD_DIR Build output directory (default: ./build) + CACHE_DIR Download cache directory (default: ./cache) + OPENWRT_DIR OpenWrt source directory for firmware builds (default: ./openwrt) + +EOF +} + +# Main script +main() { + # Change to script directory + cd "$(dirname "$0")" + + local command="${1:-help}" + shift || true + + case "$command" in + validate) + run_validation + ;; + + build) + local single_package="" + local arch_specified=false + + while [[ $# -gt 0 ]]; do + case "$1" in + --arch) + set_architecture "$2" + arch_specified=true + shift 2 + ;; + luci-app-*|luci-theme-*|secubox-app-*|secubox-*|netifyd|ndpid|nodogsplash|crowdsec|mitmproxy|metablogizer|tor|cs-firewall-bouncer) + single_package="$1" + shift + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac + done + + run_build "$single_package" + ;; + + build-firmware) + local device="$1" + if [[ -z "$device" ]]; then + print_error "Device not specified" + print_info "Usage: $0 build-firmware " + print_info "Available devices: ${!DEVICE_PROFILES[*]}" + exit 1 + fi + run_firmware_build "$device" + ;; + + build-toolchain) + local device="${1:-espressobin-v7}" + shift || true + run_toolchain_build "$device" "$@" + ;; + + debug-firmware) + local device="$1" + if [[ -z "$device" ]]; then + print_error "Device not specified" + print_info "Usage: $0 debug-firmware " + print_info "Available devices: ${!DEVICE_PROFILES[*]}" + exit 1 + fi + debug_firmware_build "$device" + ;; + + full) + run_validation && run_build + ;; + + clean) + print_header "Cleaning Build Directories" + rm -rf "$SDK_DIR" "$BUILD_DIR" + print_success "Build directories cleaned" + ;; + + clean-all) + print_header "Cleaning All Build Directories" + rm -rf "$SDK_DIR" "$BUILD_DIR" "$OPENWRT_DIR" "$CACHE_DIR" "./local-feed" + print_success "All build directories cleaned (SDK, build, OpenWrt source, cache, local-feed)" + ;; + + sync) + print_header "Synchronizing packages to local-feed" + sync_packages_to_local_feed + print_success "Packages synchronized to local-feed" + ;; + + sync-feed|regenerate-feed) + print_header "Regenerating Local Feed" + local feed_dir="$SCRIPT_DIR/../package/secubox/secubox-app-bonus/root/www/secubox-feed" + local pkg_ext="ipk" + + if [[ ! -d "$feed_dir" ]]; then + print_error "Feed directory not found: $feed_dir" + exit 1 + fi + + # Clean old versions + clean_old_ipk_versions "$feed_dir" "$pkg_ext" + + # Regenerate Packages index + print_info "Regenerating Packages index..." + ( + cd "$feed_dir" + rm -f Packages Packages.gz Packages.sig + + for pkg in *.${pkg_ext}; do + [[ -f "$pkg" ]] || continue + + local control="" + control=$(tar -xzOf "$pkg" ./control.tar.gz 2>/dev/null | tar -xzOf - ./control 2>/dev/null || \ + ar -p "$pkg" control.tar.gz 2>/dev/null | tar -xzOf - ./control 2>/dev/null || \ + ar -p "$pkg" control.tar.zst 2>/dev/null | zstd -d 2>/dev/null | tar -xOf - ./control 2>/dev/null || true) + + if [[ -n "$control" ]]; then + echo "$control" + echo "Filename: $pkg" + echo "Size: $(stat -c%s "$pkg")" + echo "SHA256sum: $(sha256sum "$pkg" | cut -d' ' -f1)" + echo "" + fi + done > Packages + + gzip -k Packages 2>/dev/null || true + ) + + # Strip libc dependency from all packages + print_info "Stripping libc dependencies..." + sed -i \ + -e 's/^Depends: libc$/Depends:/g' \ + -e 's/^Depends: libc, /Depends: /g' \ + -e 's/, libc$//g' \ + -e 's/, libc,/,/g' \ + "$feed_dir/Packages" + # Clean up any empty or malformed Depends lines + sed -i \ + -e 's/^Depends: ,/Depends: /g' \ + -e 's/, ,/, /g' \ + -e 's/,$//' \ + "$feed_dir/Packages" + gzip -kf "$feed_dir/Packages" 2>/dev/null || true + + # Generate Packages.sig + generate_packages_sig "$feed_dir" + + # Regenerate apps-local.json + PKG_EXT="$pkg_ext" generate_local_apps_json "$feed_dir" + + local pkg_count=$(ls -1 "$feed_dir"/*.${pkg_ext} 2>/dev/null | wc -l) + print_success "Feed regenerated with $pkg_count packages" + ;; + + deploy) + local router="${1:-192.168.255.1}" + local packages="$2" + print_header "Deploying Packages to Router ($router)" + deploy_packages "$router" "$packages" + ;; + + help|--help|-h) + show_usage + ;; + + *) + print_error "Unknown command: $command" + echo "" + show_usage + exit 1 + ;; + esac +} + +# Run main +main "$@" diff --git a/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users b/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users new file mode 100644 index 00000000..4ae93e06 --- /dev/null +++ b/luci-app-secubox-users/root/usr/libexec/rpcd/luci.secubox-users @@ -0,0 +1,190 @@ +#!/bin/sh +# RPCD handler for SecuBox User Management + +. /usr/share/libubox/jshn.sh + +CONFIG="secubox-users" + +uci_get() { uci -q get ${CONFIG}.$1; } + +# Check if service is running +check_service() { + local service="$1" + case "$service" in + nextcloud) [ -x /usr/sbin/nextcloudctl ] && lxc-info -n nextcloud 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;; + peertube) [ -x /usr/sbin/peertubectl ] && lxc-info -n peertube 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;; + matrix) [ -x /usr/sbin/matrixctl ] && lxc-info -n matrix 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;; + jabber) [ -x /usr/sbin/jabberctl ] && lxc-info -n jabber 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;; + email) [ -x /usr/sbin/mailserverctl ] && lxc-info -n mailserver 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0" ;; + *) echo "0" ;; + esac +} + +get_status() { + local domain=$(uci_get main.domain || echo "secubox.in") + local matrix_server=$(uci_get main.matrix_server || echo "matrix.local") + local user_count=$(uci show ${CONFIG} 2>/dev/null | grep -c "=user$" || echo 0) + + local nc_running=$(check_service nextcloud) + local pt_running=$(check_service peertube) + local mx_running=$(check_service matrix) + local jb_running=$(check_service jabber) + local em_running=$(check_service email) + + cat </dev/null | grep "=user$" | cut -d'.' -f2 | cut -d'=' -f1) + + json_init + json_add_array "users" + + for user in $users; do + json_add_object + json_add_string "username" "$user" + json_add_string "email" "$(uci_get ${user}.email)" + json_add_string "enabled" "$(uci_get ${user}.enabled)" + json_add_string "created" "$(uci_get ${user}.created)" + + # Get services as array + local services=$(uci -q get ${CONFIG}.${user}.services 2>/dev/null) + json_add_array "services" + for svc in $services; do + json_add_string "" "$svc" + done + json_close_array + + json_close_object + done + + json_close_array + json_dump +} + +add_user() { + read -r input + local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null) + local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null) + local services=$(echo "$input" | jsonfilter -e '@.services' 2>/dev/null) + + if [ -z "$username" ]; then + echo '{"success":false,"error":"Username required"}' + return + fi + + # Generate password if not provided + if [ -z "$password" ]; then + password=$(head -c 12 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 16) + fi + + # Run secubox-users add + local output + if [ -n "$services" ]; then + output=$(secubox-users add "$username" "$password" "$services" 2>&1) + else + output=$(secubox-users add "$username" "$password" 2>&1) + fi + + if echo "$output" | grep -q "USER CREDENTIALS"; then + json_init + json_add_boolean "success" 1 + json_add_string "username" "$username" + json_add_string "password" "$password" + json_add_string "email" "${username}@$(uci_get main.domain)" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Failed to create user" + json_add_string "output" "$output" + json_dump + fi +} + +delete_user() { + read -r input + local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null) + + if [ -z "$username" ]; then + echo '{"success":false,"error":"Username required"}' + return + fi + + local output=$(secubox-users del "$username" 2>&1) + + if echo "$output" | grep -q "deleted"; then + echo '{"success":true}' + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Failed to delete user" + json_add_string "output" "$output" + json_dump + fi +} + +update_password() { + read -r input + local username=$(echo "$input" | jsonfilter -e '@.username' 2>/dev/null) + local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null) + + if [ -z "$username" ]; then + echo '{"success":false,"error":"Username required"}' + return + fi + + local output + if [ -n "$password" ]; then + output=$(secubox-users passwd "$username" "$password" 2>&1) + else + output=$(secubox-users passwd "$username" 2>&1) + password=$(echo "$output" | grep "Generated password:" | cut -d: -f2 | xargs) + fi + + if echo "$output" | grep -q "Password updated"; then + json_init + json_add_boolean "success" 1 + json_add_string "password" "$password" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Failed to update password" + json_dump + fi +} + +list_methods() { + cat <<'EOFM' +{"status":{},"users":{},"add":{"username":"str","password":"str","services":"str"},"delete":{"username":"str"},"passwd":{"username":"str","password":"str"}} +EOFM +} + +case "$1" in + list) list_methods ;; + call) + case "$2" in + status) get_status ;; + users) get_users ;; + add) add_user ;; + delete) delete_user ;; + passwd) update_password ;; + *) echo '{"error":"Unknown method"}' ;; + esac + ;; + *) echo '{"error":"Unknown command"}' ;; +esac diff --git a/luci-app-secubox-users/root/usr/share/luci/menu.d/luci-app-secubox-users.json b/luci-app-secubox-users/root/usr/share/luci/menu.d/luci-app-secubox-users.json new file mode 100644 index 00000000..38e53e66 --- /dev/null +++ b/luci-app-secubox-users/root/usr/share/luci/menu.d/luci-app-secubox-users.json @@ -0,0 +1,14 @@ +{ + "admin/system/secubox-users": { + "title": "SecuBox Users", + "order": 85, + "action": { + "type": "view", + "path": "secubox-users/overview" + }, + "depends": { + "acl": ["luci-app-secubox-users"], + "uci": {"secubox-users": true} + } + } +} diff --git a/luci-app-secubox-users/root/usr/share/rpcd/acl.d/luci-app-secubox-users.json b/luci-app-secubox-users/root/usr/share/rpcd/acl.d/luci-app-secubox-users.json new file mode 100644 index 00000000..ad4b33dc --- /dev/null +++ b/luci-app-secubox-users/root/usr/share/rpcd/acl.d/luci-app-secubox-users.json @@ -0,0 +1,17 @@ +{ + "luci-app-secubox-users": { + "description": "Grant access to SecuBox User Management", + "read": { + "ubus": { + "luci.secubox-users": ["status", "users"] + }, + "uci": ["secubox-users"] + }, + "write": { + "ubus": { + "luci.secubox-users": ["add", "delete", "passwd"] + }, + "uci": ["secubox-users"] + } + } +} diff --git a/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/static.js b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/static.js new file mode 100644 index 00000000..144d2798 --- /dev/null +++ b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/static.js @@ -0,0 +1,396 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require form'; +'require fs'; + +var callStaticList = rpc.declare({ + object: 'luci.hexojs', + method: 'static_list', + params: ['instance'], + expect: {} +}); + +var callStaticUpload = rpc.declare({ + object: 'luci.hexojs', + method: 'static_upload', + params: ['instance', 'filename', 'content'], + expect: {} +}); + +var callStaticCreate = rpc.declare({ + object: 'luci.hexojs', + method: 'static_create', + params: ['name', 'domain'], + expect: {} +}); + +var callStaticDelete = rpc.declare({ + object: 'luci.hexojs', + method: 'static_delete', + params: ['name'], + expect: {} +}); + +var callStaticPublish = rpc.declare({ + object: 'luci.hexojs', + method: 'static_publish', + params: ['instance'], + expect: {} +}); + +var callStaticDeleteFile = rpc.declare({ + object: 'luci.hexojs', + method: 'static_delete_file', + params: ['instance', 'filename'], + expect: {} +}); + +var callStaticConfigureAuth = rpc.declare({ + object: 'luci.hexojs', + method: 'static_configure_auth', + params: ['instance', 'enabled', 'domain'], + expect: {} +}); + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function formatDate(timestamp) { + if (!timestamp) return '-'; + var d = new Date(timestamp * 1000); + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); +} + +return view.extend({ + selectedInstance: null, + + load: function() { + return callStaticList({}); + }, + + renderInstanceCard: function(instance) { + var self = this; + var card = E('div', { 'class': 'cbi-section', 'style': 'margin-bottom: 10px; padding: 15px; border: 1px solid #ccc; border-radius: 8px;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;' }, [ + E('div', {}, [ + E('strong', { 'style': 'font-size: 1.2em;' }, instance.name), + instance.domain ? E('span', { 'style': 'margin-left: 10px; color: #666;' }, instance.domain) : '', + instance.auth_enabled ? E('span', { 'class': 'badge', 'style': 'margin-left: 10px; background: #28a745; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.8em;' }, 'Auth') : '' + ]), + E('div', {}, [ + E('span', { 'style': 'margin-right: 15px; color: #666;' }, instance.file_count + ' files'), + instance.port > 0 ? E('span', { 'style': 'color: #666;' }, 'Port ' + instance.port) : '' + ]) + ]), + E('div', { 'style': 'display: flex; gap: 10px;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.showFiles(instance.name); } + }, 'Manage Files'), + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': function() { self.publishSite(instance.name); } + }, 'Publish'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.configureAuth(instance.name, instance.domain); } + }, 'Auth'), + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': function() { self.deleteSite(instance.name); } + }, 'Delete') + ]) + ]); + return card; + }, + + showFiles: function(instanceName) { + var self = this; + + callStaticList({ instance: instanceName }).then(function(result) { + if (!result.success) { + ui.addNotification(null, E('p', result.error || 'Failed to list files'), 'error'); + return; + } + + self.selectedInstance = instanceName; + var container = document.getElementById('file-list-container'); + container.innerHTML = ''; + + // Header + container.appendChild(E('h3', {}, 'Files in "' + instanceName + '"')); + + // Upload section + var uploadSection = E('div', { 'class': 'cbi-section', 'style': 'padding: 15px; background: #f9f9f9; border-radius: 8px; margin-bottom: 15px;' }, [ + E('h4', {}, 'Upload Files'), + E('input', { + 'type': 'file', + 'id': 'file-upload-input', + 'multiple': true, + 'style': 'margin-right: 10px;' + }), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.uploadFiles(instanceName); } + }, 'Upload Selected') + ]); + container.appendChild(uploadSection); + + // File list table + var table = E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Filename'), + E('th', { 'class': 'th' }, 'Size'), + E('th', { 'class': 'th' }, 'Modified'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]); + + var files = result.files || []; + if (files.length === 0) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '4', 'style': 'text-align: center; color: #666;' }, 'No files yet. Upload some files above.') + ])); + } else { + files.forEach(function(file) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, file.name), + E('td', { 'class': 'td' }, formatBytes(file.size)), + E('td', { 'class': 'td' }, formatDate(file.modified)), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'style': 'padding: 2px 8px; font-size: 0.9em;', + 'click': function() { self.deleteFile(instanceName, file.name); } + }, 'Delete') + ]) + ])); + }); + } + + container.appendChild(table); + + // Back button + container.appendChild(E('button', { + 'class': 'cbi-button', + 'style': 'margin-top: 15px;', + 'click': function() { self.refreshView(); } + }, 'Back to Sites')); + }); + }, + + uploadFiles: function(instanceName) { + var self = this; + var input = document.getElementById('file-upload-input'); + var files = input.files; + + if (files.length === 0) { + ui.addNotification(null, E('p', 'Please select files to upload'), 'warning'); + return; + } + + var uploadPromises = []; + + for (var i = 0; i < files.length; i++) { + (function(file) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(e) { + // Convert to base64 + var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(e.target.result))); + + callStaticUpload({ + instance: instanceName, + filename: file.name, + content: base64 + }).then(function(result) { + if (result.success) { + resolve(file.name); + } else { + reject(result.error || 'Upload failed for ' + file.name); + } + }).catch(reject); + }; + reader.onerror = function() { + reject('Failed to read file: ' + file.name); + }; + reader.readAsArrayBuffer(file); + }); + uploadPromises.push(promise); + })(files[i]); + } + + Promise.all(uploadPromises).then(function(uploaded) { + ui.addNotification(null, E('p', 'Uploaded ' + uploaded.length + ' file(s)'), 'success'); + self.showFiles(instanceName); + }).catch(function(err) { + ui.addNotification(null, E('p', 'Upload error: ' + err), 'error'); + self.showFiles(instanceName); + }); + }, + + deleteFile: function(instanceName, filename) { + var self = this; + + if (!confirm('Delete file "' + filename + '"?')) return; + + callStaticDeleteFile({ instance: instanceName, filename: filename }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'File deleted'), 'success'); + self.showFiles(instanceName); + } else { + ui.addNotification(null, E('p', result.error || 'Failed to delete file'), 'error'); + } + }); + }, + + publishSite: function(instanceName) { + callStaticPublish({ instance: instanceName }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'Published to ' + result.url), 'success'); + } else { + ui.addNotification(null, E('p', result.error || 'Failed to publish'), 'error'); + } + }); + }, + + configureAuth: function(instanceName, currentDomain) { + var self = this; + + var domain = prompt('Enter domain for HAProxy auth (e.g., site.example.com):', currentDomain || ''); + if (domain === null) return; + + if (!domain) { + ui.addNotification(null, E('p', 'Domain is required for HAProxy auth'), 'warning'); + return; + } + + callStaticConfigureAuth({ + instance: instanceName, + enabled: true, + domain: domain + }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'Auth configured for ' + domain), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', result.error || 'Failed to configure auth'), 'error'); + } + }); + }, + + deleteSite: function(instanceName) { + var self = this; + + if (!confirm('Delete static site "' + instanceName + '" and all its files?')) return; + + callStaticDelete({ name: instanceName }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'Site deleted'), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', result.error || 'Failed to delete site'), 'error'); + } + }); + }, + + createSite: function() { + var self = this; + + var name = prompt('Enter site name (lowercase, no spaces):'); + if (!name) return; + + // Validate name + if (!/^[a-z][a-z0-9_]*$/.test(name)) { + ui.addNotification(null, E('p', 'Invalid name. Use lowercase letters, numbers, underscore.'), 'error'); + return; + } + + var domain = prompt('Enter domain (optional, e.g., site.example.com):'); + + callStaticCreate({ name: name, domain: domain || '' }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'Site "' + name + '" created on port ' + result.port), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', result.error || 'Failed to create site'), 'error'); + } + }); + }, + + refreshView: function() { + var self = this; + + callStaticList({}).then(function(result) { + var container = document.getElementById('file-list-container'); + container.innerHTML = ''; + + var instancesContainer = document.getElementById('static-instances'); + instancesContainer.innerHTML = ''; + + if (!result.success) { + instancesContainer.appendChild(E('p', { 'style': 'color: red;' }, result.error || 'Failed to load sites')); + return; + } + + var instances = result.instances || []; + if (instances.length === 0) { + instancesContainer.appendChild(E('p', { 'style': 'color: #666; text-align: center; padding: 20px;' }, + 'No static sites yet. Click "Create New Site" to get started.')); + } else { + instances.forEach(function(inst) { + instancesContainer.appendChild(self.renderInstanceCard(inst)); + }); + } + }); + }, + + render: function(data) { + var self = this; + var instances = (data && data.instances) || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'class': 'cbi-map-title' }, 'Static Sites'), + E('div', { 'class': 'cbi-map-descr' }, + 'Upload and manage static HTML sites with optional Basic Auth via HAProxy. Fast KISS publishing without Hexo build process.'), + + // Create button + E('div', { 'style': 'margin: 20px 0;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { self.createSite(); } + }, 'Create New Site') + ]), + + // Instances container + E('div', { 'id': 'static-instances' }), + + // File list container (shown when managing a site) + E('div', { 'id': 'file-list-container', 'style': 'margin-top: 20px;' }) + ]); + + // Render initial instances + var instancesContainer = view.querySelector('#static-instances'); + if (instances.length === 0) { + instancesContainer.appendChild(E('p', { 'style': 'color: #666; text-align: center; padding: 20px;' }, + 'No static sites yet. Click "Create New Site" to get started.')); + } else { + instances.forEach(function(inst) { + instancesContainer.appendChild(self.renderInstanceCard(inst)); + }); + } + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs b/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs index 48ea5a7c..72b0f8dd 100755 --- a/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs +++ b/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs @@ -3163,6 +3163,295 @@ delete_backup() { json_dump } +# ============================================ +# Static Site Methods (KISS Upload) +# ============================================ + +# List static sites or files in a site +static_list() { + read input + json_load "$input" + json_get_var instance instance + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + + json_init + json_add_boolean "success" 1 + + if [ -n "$instance" ]; then + # List files in specific instance + local static_dir="$data_path/static/$instance" + if [ ! -d "$static_dir" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Static instance '$instance' not found" + json_dump + return + fi + + json_add_string "instance" "$instance" + json_add_array "files" + + for f in "$static_dir"/*; do + [ -f "$f" ] || continue + local filename=$(basename "$f") + local size=$(stat -c%s "$f" 2>/dev/null || echo 0) + local modified=$(stat -c%Y "$f" 2>/dev/null || echo 0) + + json_add_object + json_add_string "name" "$filename" + json_add_int "size" "$size" + json_add_int "modified" "$modified" + json_close_object + done + + json_close_array + else + # List all static instances + json_add_array "instances" + + for dir in "$data_path/static"/*; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + local count=$(find "$dir" -type f 2>/dev/null | wc -l) + local port=$(uci -q get hexojs.${name}.port) + local domain=$(uci -q get hexojs.${name}.domain) + local auth=$(uci -q get hexojs.${name}.auth_enabled) + + json_add_object + json_add_string "name" "$name" + json_add_int "file_count" "$count" + json_add_int "port" "${port:-0}" + json_add_string "domain" "$domain" + json_add_boolean "auth_enabled" "${auth:-0}" + json_close_object + done + + json_close_array + fi + + json_dump +} + +# Upload file to static site (base64 encoded) +static_upload() { + read input + json_load "$input" + json_get_var instance instance + json_get_var filename filename + json_get_var content content + + json_init + + if [ -z "$filename" ] || [ -z "$content" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Filename and content required" + json_dump + return + fi + + [ -z "$instance" ] && instance="default" + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + local static_dir="$data_path/static/$instance" + + # Auto-create instance if needed + if [ ! -d "$static_dir" ]; then + "$HEXOCTL" static create "$instance" >/dev/null 2>&1 + fi + + # Sanitize filename (prevent path traversal) + local safe_filename=$(basename "$filename" | tr -cd 'a-zA-Z0-9._-') + local target_path="$static_dir/$safe_filename" + + # Decode base64 content and save + echo "$content" | base64 -d > "$target_path" 2>/dev/null + local result=$? + + if [ "$result" -eq 0 ] && [ -f "$target_path" ]; then + local size=$(stat -c%s "$target_path" 2>/dev/null || echo 0) + json_add_boolean "success" 1 + json_add_string "message" "File uploaded" + json_add_string "filename" "$safe_filename" + json_add_string "path" "$target_path" + json_add_int "size" "$size" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to save file" + fi + + json_dump +} + +# Create static site instance +static_create() { + read input + json_load "$input" + json_get_var name name + json_get_var domain domain + + json_init + + if [ -z "$name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance name required" + json_dump + return + fi + + local output=$("$HEXOCTL" static create "$name" 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + # Set domain if provided + [ -n "$domain" ] && uci set hexojs.${name}.domain="$domain" && uci commit hexojs + + local port=$(uci -q get hexojs.${name}.port) + json_add_boolean "success" 1 + json_add_string "message" "Static instance created" + json_add_string "name" "$name" + json_add_int "port" "$port" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# Delete static site instance +static_delete() { + read input + json_load "$input" + json_get_var name name + + json_init + + if [ -z "$name" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance name required" + json_dump + return + fi + + local output=$("$HEXOCTL" static delete "$name" 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Static instance deleted" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# Publish static site to /www/static/ +static_publish() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + [ -z "$instance" ] && instance="default" + + local output=$("$HEXOCTL" static publish "$instance" 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Published to /www/static/$instance" + json_add_string "url" "/static/$instance/" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# Delete file from static site +static_delete_file() { + read input + json_load "$input" + json_get_var instance instance + json_get_var filename filename + + json_init + + if [ -z "$instance" ] || [ -z "$filename" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance and filename required" + json_dump + return + fi + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + local static_dir="$data_path/static/$instance" + local safe_filename=$(basename "$filename") + local target="$static_dir/$safe_filename" + + if [ -f "$target" ]; then + rm -f "$target" + json_add_boolean "success" 1 + json_add_string "message" "File deleted" + else + json_add_boolean "success" 0 + json_add_string "error" "File not found" + fi + + json_dump +} + +# Configure HAProxy auth for static site +static_configure_auth() { + read input + json_load "$input" + json_get_var instance instance + json_get_var enabled enabled + json_get_var domain domain + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + [ -z "$domain" ] && domain=$(uci -q get hexojs.${instance}.domain) + + if [ -z "$domain" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Domain required for HAProxy auth" + json_dump + return + fi + + # Update UCI config + uci set hexojs.${instance}.auth_enabled="${enabled:-1}" + uci set hexojs.${instance}.domain="$domain" + uci commit hexojs + + # Apply auth via hexoctl + local output=$("$HEXOCTL" auth apply "$instance" 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Auth configured for $domain" + json_add_string "domain" "$domain" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + # ============================================ # GitHub Integration Methods # ============================================ @@ -3379,7 +3668,14 @@ case "$1" in "handle_webhook": {"event": "str", "repository": "str", "ref": "str", "secret": "str"}, "setup_webhook": {"auto_build": "bool", "webhook_secret": "str"}, "get_instance_health": {"instance": "str"}, - "get_pipeline_status": {} + "get_pipeline_status": {}, + "static_list": {"instance": "str"}, + "static_upload": {"instance": "str", "filename": "str", "content": "str"}, + "static_create": {"name": "str", "domain": "str"}, + "static_delete": {"name": "str"}, + "static_publish": {"instance": "str"}, + "static_delete_file": {"instance": "str", "filename": "str"}, + "static_configure_auth": {"instance": "str", "enabled": "bool", "domain": "str"} } EOF ;; @@ -3459,6 +3755,13 @@ EOF setup_webhook) setup_webhook ;; get_instance_health) get_instance_health ;; get_pipeline_status) get_pipeline_status ;; + static_list) static_list ;; + static_upload) static_upload ;; + static_create) static_create ;; + static_delete) static_delete ;; + static_publish) static_publish ;; + static_delete_file) static_delete_file ;; + static_configure_auth) static_configure_auth ;; *) echo '{"error": "Unknown method"}' ;; esac ;; diff --git a/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json b/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json index 55c1bfcb..3a86ae3e 100644 --- a/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json +++ b/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json @@ -105,5 +105,13 @@ "type": "view", "path": "hexojs/settings" } + }, + "admin/services/hexojs/static": { + "title": "Static Sites", + "order": 115, + "action": { + "type": "view", + "path": "hexojs/static" + } } } diff --git a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json index 08bb3d5e..6e08713f 100644 --- a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json +++ b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json @@ -31,7 +31,8 @@ "get_tor_status", "get_instance_endpoints", "get_instance_health", - "get_pipeline_status" + "get_pipeline_status", + "static_list" ] }, "uci": ["hexojs"] @@ -84,7 +85,13 @@ "unpublish_from_tor", "full_publish", "handle_webhook", - "setup_webhook" + "setup_webhook", + "static_upload", + "static_create", + "static_delete", + "static_publish", + "static_delete_file", + "static_configure_auth" ] }, "uci": ["hexojs"] diff --git a/package/secubox/luci-app-jitsi/htdocs/luci-static/resources/view/jitsi/config.js b/package/secubox/luci-app-jitsi/htdocs/luci-static/resources/view/jitsi/config.js index 4da6cbcf..8bf9f816 100644 --- a/package/secubox/luci-app-jitsi/htdocs/luci-static/resources/view/jitsi/config.js +++ b/package/secubox/luci-app-jitsi/htdocs/luci-static/resources/view/jitsi/config.js @@ -294,6 +294,8 @@ return view.extend({ ''; }; - return KissTheme.wrap([m.render()], 'admin/services/jitsi'); + return m.render().then(function(mapEl) { + return KissTheme.wrap([mapEl], 'admin/services/jitsi'); + }); } }); diff --git a/package/secubox/luci-app-media-hub/Makefile b/package/secubox/luci-app-media-hub/Makefile new file mode 100644 index 00000000..5ca46bf6 --- /dev/null +++ b/package/secubox/luci-app-media-hub/Makefile @@ -0,0 +1,29 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-media-hub +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI Media Services Hub +LUCI_DEPENDS:=+luci-base + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-media-hub/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.media-hub $(1)/usr/libexec/rpcd/luci.media-hub + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-media-hub.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-media-hub.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/media-hub + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/media-hub/*.js $(1)/www/luci-static/resources/view/media-hub/ +endef + +$(eval $(call BuildPackage,luci-app-media-hub)) diff --git a/package/secubox/luci-app-media-hub/htdocs/luci-static/resources/view/media-hub/dashboard.js b/package/secubox/luci-app-media-hub/htdocs/luci-static/resources/view/media-hub/dashboard.js new file mode 100644 index 00000000..a3813ef9 --- /dev/null +++ b/package/secubox/luci-app-media-hub/htdocs/luci-static/resources/view/media-hub/dashboard.js @@ -0,0 +1,376 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callMediaHubStatus = rpc.declare({ + object: 'luci.media-hub', + method: 'status', + expect: { '': {} } +}); + +var callMediaHubServices = rpc.declare({ + object: 'luci.media-hub', + method: 'services', + expect: { services: [] } +}); + +var callServiceStart = rpc.declare({ + object: 'luci.media-hub', + method: 'service_start', + params: ['id'] +}); + +var callServiceStop = rpc.declare({ + object: 'luci.media-hub', + method: 'service_stop', + params: ['id'] +}); + +var callServiceRestart = rpc.declare({ + object: 'luci.media-hub', + method: 'service_restart', + params: ['id'] +}); + +// Category icons and colors +var categoryConfig = { + streaming: { icon: '🎬', color: '#e74c3c', label: 'Streaming' }, + conferencing: { icon: '📹', color: '#3498db', label: 'Conferencing' }, + apps: { icon: '📊', color: '#9b59b6', label: 'Apps' }, + display: { icon: '🪞', color: '#1abc9c', label: 'Display' }, + social: { icon: '🦣', color: '#e67e22', label: 'Social' }, + monitoring: { icon: '📡', color: '#2ecc71', label: 'Monitoring' } +}; + +// Status colors and icons +var statusConfig = { + running: { color: '#27ae60', icon: '●', label: 'Running' }, + stopped: { color: '#e74c3c', icon: '○', label: 'Stopped' }, + not_installed: { color: '#95a5a6', icon: '◌', label: 'Not Installed' }, + unknown: { color: '#f39c12', icon: '?', label: 'Unknown' } +}; + +return view.extend({ + load: function() { + return Promise.all([ + callMediaHubStatus(), + callMediaHubServices() + ]); + }, + + renderStatusBar: function(status) { + var total = status.total_services || 0; + var installed = status.installed || 0; + var running = status.running || 0; + var stopped = status.stopped || 0; + + return E('div', { 'class': 'media-hub-status-bar' }, [ + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #3498db' }, String(total)), + E('span', { 'class': 'status-label' }, 'Total') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #9b59b6' }, String(installed)), + E('span', { 'class': 'status-label' }, 'Installed') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #27ae60' }, String(running)), + E('span', { 'class': 'status-label' }, 'Running') + ]), + E('div', { 'class': 'status-item' }, [ + E('span', { 'class': 'status-value', 'style': 'color: #e74c3c' }, String(stopped)), + E('span', { 'class': 'status-label' }, 'Stopped') + ]) + ]); + }, + + renderServiceCard: function(service) { + var self = this; + var statusCfg = statusConfig[service.status] || statusConfig.unknown; + var categoryCfg = categoryConfig[service.category] || { icon: '📦', color: '#7f8c8d', label: 'Other' }; + + var controls = []; + + if (service.installed) { + if (service.status === 'running') { + controls.push( + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callServiceStop(service.id).then(function() { + window.location.reload(); + }); + }) + }, '⏹ Stop'), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callServiceRestart(service.id).then(function() { + window.location.reload(); + }); + }) + }, '🔄 Restart') + ); + } else if (service.status === 'stopped') { + controls.push( + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'style': 'padding: 4px 12px; font-size: 12px;', + 'click': ui.createHandlerFn(this, function() { + return callServiceStart(service.id).then(function() { + window.location.reload(); + }); + }) + }, '▶ Start') + ); + } + } + + // Add settings link + if (service.url) { + controls.push( + E('a', { + 'href': service.url, + 'class': 'cbi-button cbi-button-neutral', + 'style': 'margin-left: 5px; padding: 4px 12px; font-size: 12px; text-decoration: none;' + }, '⚙ Settings') + ); + } + + return E('div', { + 'class': 'media-service-card', + 'data-status': service.status, + 'style': 'border-left: 4px solid ' + categoryCfg.color + ';' + }, [ + E('div', { 'class': 'card-header' }, [ + E('span', { 'class': 'service-emoji' }, service.emoji || '📦'), + E('div', { 'class': 'service-info' }, [ + E('span', { 'class': 'service-name' }, service.name), + E('span', { 'class': 'service-category', 'style': 'color: ' + categoryCfg.color }, categoryCfg.label) + ]), + E('span', { + 'class': 'service-status', + 'style': 'color: ' + statusCfg.color, + 'title': statusCfg.label + }, statusCfg.icon + ' ' + statusCfg.label) + ]), + E('div', { 'class': 'card-body' }, [ + E('p', { 'class': 'service-description' }, service.description), + service.port > 0 ? E('p', { 'class': 'service-port' }, 'Port: ' + service.port) : E('span') + ]), + E('div', { 'class': 'card-footer' }, controls) + ]); + }, + + renderCategorySection: function(category, services) { + var categoryCfg = categoryConfig[category] || { icon: '📦', color: '#7f8c8d', label: category }; + var categoryServices = services.filter(function(s) { return s.category === category; }); + + if (categoryServices.length === 0) return E('span'); + + var self = this; + return E('div', { 'class': 'media-category-section' }, [ + E('h3', { 'class': 'category-title', 'style': 'color: ' + categoryCfg.color }, [ + E('span', { 'class': 'category-icon' }, categoryCfg.icon), + ' ', + categoryCfg.label, + E('span', { 'class': 'category-count' }, ' (' + categoryServices.length + ')') + ]), + E('div', { 'class': 'media-cards-grid' }, + categoryServices.map(function(service) { + return self.renderServiceCard(service); + }) + ) + ]); + }, + + render: function(data) { + var status = data[0]; + var services = data[1]; + + // Sort by category then by name + services.sort(function(a, b) { + if (a.category !== b.category) { + return a.category.localeCompare(b.category); + } + return a.name.localeCompare(b.name); + }); + + // Get unique categories in order + var categories = ['streaming', 'conferencing', 'apps', 'display', 'social', 'monitoring']; + + var self = this; + var view = E('div', { 'class': 'media-hub-dashboard' }, [ + E('style', {}, ` + .media-hub-dashboard { + padding: 20px; + } + .media-hub-header { + text-align: center; + margin-bottom: 30px; + } + .media-hub-header h2 { + font-size: 2em; + margin-bottom: 10px; + } + .media-hub-header .subtitle { + color: #666; + font-size: 1.1em; + } + .media-hub-status-bar { + display: flex; + justify-content: center; + gap: 40px; + padding: 20px; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border-radius: 12px; + margin-bottom: 30px; + } + .status-item { + text-align: center; + } + .status-value { + display: block; + font-size: 2.5em; + font-weight: bold; + } + .status-label { + color: #aaa; + font-size: 0.9em; + text-transform: uppercase; + } + .media-category-section { + margin-bottom: 30px; + } + .category-title { + font-size: 1.4em; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #333; + } + .category-icon { + font-size: 1.2em; + } + .category-count { + font-weight: normal; + font-size: 0.8em; + color: #666; + } + .media-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + } + .media-service-card { + background: #1a1a2e; + border-radius: 12px; + padding: 20px; + transition: transform 0.2s, box-shadow 0.2s; + } + .media-service-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0,0,0,0.3); + } + .media-service-card[data-status="running"] { + background: linear-gradient(135deg, #1a2e1a 0%, #162e16 100%); + } + .media-service-card[data-status="stopped"] { + background: linear-gradient(135deg, #2e1a1a 0%, #2e1616 100%); + } + .media-service-card[data-status="not_installed"] { + opacity: 0.7; + } + .card-header { + display: flex; + align-items: center; + margin-bottom: 15px; + } + .service-emoji { + font-size: 2.5em; + margin-right: 15px; + } + .service-info { + flex: 1; + } + .service-name { + display: block; + font-size: 1.3em; + font-weight: bold; + color: #fff; + } + .service-category { + font-size: 0.85em; + text-transform: uppercase; + } + .service-status { + font-size: 0.9em; + font-weight: bold; + } + .card-body { + margin-bottom: 15px; + } + .service-description { + color: #aaa; + font-size: 0.95em; + margin-bottom: 8px; + } + .service-port { + color: #666; + font-size: 0.85em; + font-family: monospace; + } + .card-footer { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .card-footer button, .card-footer a { + border-radius: 6px; + } + @media (max-width: 768px) { + .media-hub-status-bar { + flex-wrap: wrap; + gap: 20px; + } + .media-cards-grid { + grid-template-columns: 1fr; + } + } + `), + E('div', { 'class': 'media-hub-header' }, [ + E('h2', {}, '🎬 Media Services Hub'), + E('p', { 'class': 'subtitle' }, 'Unified dashboard for all SecuBox media services') + ]), + this.renderStatusBar(status) + ]); + + categories.forEach(function(category) { + var section = self.renderCategorySection(category, services); + if (section.tagName !== 'SPAN') { + view.appendChild(section); + } + }); + + // Setup polling for status updates + poll.add(L.bind(function() { + return callMediaHubServices().then(L.bind(function(services) { + // Update status indicators without full reload + services.forEach(function(service) { + var card = document.querySelector('.media-service-card[data-status]'); + // Could update individual cards here for smooth updates + }); + }, this)); + }, this), 30); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-media-hub/root/usr/libexec/rpcd/luci.media-hub b/package/secubox/luci-app-media-hub/root/usr/libexec/rpcd/luci.media-hub new file mode 100755 index 00000000..06b4edb4 --- /dev/null +++ b/package/secubox/luci-app-media-hub/root/usr/libexec/rpcd/luci.media-hub @@ -0,0 +1,523 @@ +#!/bin/sh +# Media Hub RPCD backend - Unified media services dashboard + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +# Service definitions: id|name|emoji|port|ctl_cmd|container|category +MEDIA_SERVICES=" +jellyfin|Jellyfin|🎬|8096|jellyfinctl|jellyfin|streaming +lyrion|Lyrion Music|🎵|9000|lyrionctl|lyrion|streaming +jitsi|Jitsi Meet|📹|8443|jitsctl|jitsi-web|conferencing +peertube|PeerTube|📺|9000|peertubectl|peertube|streaming +streamlit|Streamlit|📊|8501|streamlitctl|streamlit|apps +magicmirror|MagicMirror²|🪞|8080|mmctl|magicmirror|display +gotosocial|GoToSocial|🦣|8080|gotosocialctl|gotosocial|social +" + +# Check if a service is installed +_service_installed() { + local ctl="$1" + command -v "$ctl" >/dev/null 2>&1 +} + +# Check container status +_container_status() { + local name="$1" + if command -v lxc-info >/dev/null 2>&1; then + lxc-info -n "$name" -s 2>/dev/null | grep -q "RUNNING" && echo "running" && return + [ -d "/srv/lxc/$name" ] && echo "stopped" && return + fi + if command -v docker >/dev/null 2>&1; then + docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$" && echo "running" && return + docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$" && echo "stopped" && return + fi + echo "not_installed" +} + +# Check if port is listening +_port_listening() { + local port="$1" + netstat -tln 2>/dev/null | grep -q ":${port} " && echo "1" || echo "0" +} + +# Get service status via its control command +_get_service_status() { + local ctl="$1" + local output + + if ! _service_installed "$ctl"; then + echo "not_installed" + return + fi + + # Try to get status from the service's ctl command + output=$("$ctl" status 2>/dev/null | head -5) + if echo "$output" | grep -qi "running\|active\|started"; then + echo "running" + elif echo "$output" | grep -qi "stopped\|inactive\|not running"; then + echo "stopped" + else + echo "unknown" + fi +} + +# =========================================== +# Status - Overall dashboard status +# =========================================== + +get_status() { + json_init + + local total=0 + local running=0 + local stopped=0 + local not_installed=0 + + echo "$MEDIA_SERVICES" | while IFS='|' read -r id name emoji port ctl container category; do + [ -z "$id" ] && continue + total=$((total + 1)) + + if ! _service_installed "$ctl"; then + not_installed=$((not_installed + 1)) + elif [ "$(_port_listening "$port")" = "1" ]; then + running=$((running + 1)) + else + stopped=$((stopped + 1)) + fi + done + + # Count installed services + local installed=0 + local active=0 + for svc in jellyfin lyrion jitsi peertube streamlit magicmirror gotosocial; do + case "$svc" in + jellyfin) [ -x /usr/sbin/jellyfinctl ] && installed=$((installed+1)) && netstat -tln 2>/dev/null | grep -q ":8096 " && active=$((active+1)) ;; + lyrion) [ -x /usr/sbin/lyrionctl ] && installed=$((installed+1)) && netstat -tln 2>/dev/null | grep -q ":9000 " && active=$((active+1)) ;; + jitsi) [ -x /usr/sbin/jitsctl ] && installed=$((installed+1)) && netstat -tln 2>/dev/null | grep -q ":8443 " && active=$((active+1)) ;; + peertube) [ -x /usr/sbin/peertubectl ] && installed=$((installed+1)) ;; + streamlit) [ -x /usr/sbin/streamlitctl ] && installed=$((installed+1)) && netstat -tln 2>/dev/null | grep -q ":8501 " && active=$((active+1)) ;; + magicmirror) [ -x /usr/sbin/mmctl ] && installed=$((installed+1)) ;; + gotosocial) [ -x /usr/sbin/gotosocialctl ] && installed=$((installed+1)) ;; + esac + done + + json_add_int "total_services" 7 + json_add_int "installed" "$installed" + json_add_int "running" "$active" + json_add_int "stopped" "$((installed - active))" + + json_dump +} + +# =========================================== +# Services - List all media services +# =========================================== + +get_services() { + json_init + json_add_array "services" + + # Jellyfin + json_add_object + json_add_string "id" "jellyfin" + json_add_string "name" "Jellyfin" + json_add_string "emoji" "🎬" + json_add_string "description" "Media server for movies, TV, music & photos" + json_add_string "category" "streaming" + json_add_int "port" 8096 + json_add_string "ctl" "jellyfinctl" + if [ -x /usr/sbin/jellyfinctl ]; then + json_add_boolean "installed" 1 + if netstat -tln 2>/dev/null | grep -q ":8096 "; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/jellyfin" + json_close_object + + # Lyrion + json_add_object + json_add_string "id" "lyrion" + json_add_string "name" "Lyrion Music Server" + json_add_string "emoji" "🎵" + json_add_string "description" "Music streaming (Squeezebox/LMS)" + json_add_string "category" "streaming" + json_add_int "port" 9000 + json_add_string "ctl" "lyrionctl" + if [ -x /usr/sbin/lyrionctl ]; then + json_add_boolean "installed" 1 + if netstat -tln 2>/dev/null | grep -q ":9000 "; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/lyrion" + json_close_object + + # Jitsi Meet + json_add_object + json_add_string "id" "jitsi" + json_add_string "name" "Jitsi Meet" + json_add_string "emoji" "📹" + json_add_string "description" "Video conferencing with E2E encryption" + json_add_string "category" "conferencing" + json_add_int "port" 8443 + json_add_string "ctl" "jitsctl" + if [ -x /usr/sbin/jitsctl ]; then + json_add_boolean "installed" 1 + # Check if jitsi containers are running + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "jitsi"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/jitsi" + json_close_object + + # PeerTube + json_add_object + json_add_string "id" "peertube" + json_add_string "name" "PeerTube" + json_add_string "emoji" "📺" + json_add_string "description" "Federated video platform" + json_add_string "category" "streaming" + json_add_int "port" 9000 + json_add_string "ctl" "peertubectl" + if [ -x /usr/sbin/peertubectl ]; then + json_add_boolean "installed" 1 + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "peertube"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/peertube" + json_close_object + + # Streamlit + json_add_object + json_add_string "id" "streamlit" + json_add_string "name" "Streamlit Apps" + json_add_string "emoji" "📊" + json_add_string "description" "Python data apps platform" + json_add_string "category" "apps" + json_add_int "port" 8501 + json_add_string "ctl" "streamlitctl" + if [ -x /usr/sbin/streamlitctl ]; then + json_add_boolean "installed" 1 + if lxc-info -n streamlit -s 2>/dev/null | grep -q "RUNNING"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/streamlit" + json_close_object + + # MagicMirror² + json_add_object + json_add_string "id" "magicmirror" + json_add_string "name" "MagicMirror²" + json_add_string "emoji" "🪞" + json_add_string "description" "Smart mirror display platform" + json_add_string "category" "display" + json_add_int "port" 8080 + json_add_string "ctl" "mmctl" + if [ -x /usr/sbin/mmctl ]; then + json_add_boolean "installed" 1 + if lxc-info -n magicmirror -s 2>/dev/null | grep -q "RUNNING" || docker ps --format '{{.Names}}' 2>/dev/null | grep -q "magicmirror"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/magicmirror2" + json_close_object + + # GoToSocial + json_add_object + json_add_string "id" "gotosocial" + json_add_string "name" "GoToSocial" + json_add_string "emoji" "🦣" + json_add_string "description" "Fediverse social network" + json_add_string "category" "social" + json_add_int "port" 8080 + json_add_string "ctl" "gotosocialctl" + if [ -x /usr/sbin/gotosocialctl ]; then + json_add_boolean "installed" 1 + if lxc-info -n gotosocial -s 2>/dev/null | grep -q "RUNNING" || docker ps --format '{{.Names}}' 2>/dev/null | grep -q "gotosocial"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/gotosocial" + json_close_object + + # Media Flow (monitoring) + json_add_object + json_add_string "id" "media-flow" + json_add_string "name" "Media Flow" + json_add_string "emoji" "📡" + json_add_string "description" "Stream detection & monitoring" + json_add_string "category" "monitoring" + json_add_int "port" 0 + json_add_string "ctl" "" + # Media Flow is always "installed" if the menu exists + if [ -f /usr/share/luci/menu.d/luci-app-media-flow.json ]; then + json_add_boolean "installed" 1 + json_add_string "status" "running" + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + json_add_string "url" "/cgi-bin/luci/admin/services/media-flow" + json_close_object + + json_close_array + json_dump +} + +# =========================================== +# Service Status - Get detailed status for one service +# =========================================== + +get_service_status() { + read input + json_load "$input" + json_get_var service_id id + + json_init + + case "$service_id" in + jellyfin) + if [ -x /usr/sbin/jellyfinctl ]; then + json_add_boolean "installed" 1 + local status=$(jellyfinctl status 2>/dev/null) + if echo "$status" | grep -qi "running"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + json_add_string "details" "$status" + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + ;; + lyrion) + if [ -x /usr/sbin/lyrionctl ]; then + json_add_boolean "installed" 1 + local status=$(lyrionctl status 2>/dev/null) + if echo "$status" | grep -qi "running"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + json_add_string "details" "$status" + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + ;; + jitsi) + if [ -x /usr/sbin/jitsctl ]; then + json_add_boolean "installed" 1 + local status=$(jitsctl status 2>/dev/null) + if echo "$status" | grep -qi "running"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + json_add_string "details" "$status" + else + json_add_boolean "installed" 0 + json_add_string "status" "not_installed" + fi + ;; + *) + json_add_boolean "installed" 0 + json_add_string "status" "unknown" + json_add_string "error" "Unknown service: $service_id" + ;; + esac + + json_dump +} + +# =========================================== +# Service Control +# =========================================== + +service_start() { + read input + json_load "$input" + json_get_var service_id id + + json_init + + local ctl="" + case "$service_id" in + jellyfin) ctl="jellyfinctl" ;; + lyrion) ctl="lyrionctl" ;; + jitsi) ctl="jitsctl" ;; + peertube) ctl="peertubectl" ;; + streamlit) ctl="streamlitctl" ;; + magicmirror) ctl="mmctl" ;; + gotosocial) ctl="gotosocialctl" ;; + esac + + if [ -z "$ctl" ] || ! command -v "$ctl" >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "Service not installed or unknown" + json_dump + return + fi + + local output=$("$ctl" start 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Service started" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +service_stop() { + read input + json_load "$input" + json_get_var service_id id + + json_init + + local ctl="" + case "$service_id" in + jellyfin) ctl="jellyfinctl" ;; + lyrion) ctl="lyrionctl" ;; + jitsi) ctl="jitsctl" ;; + peertube) ctl="peertubectl" ;; + streamlit) ctl="streamlitctl" ;; + magicmirror) ctl="mmctl" ;; + gotosocial) ctl="gotosocialctl" ;; + esac + + if [ -z "$ctl" ] || ! command -v "$ctl" >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "Service not installed or unknown" + json_dump + return + fi + + local output=$("$ctl" stop 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Service stopped" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +service_restart() { + read input + json_load "$input" + json_get_var service_id id + + json_init + + local ctl="" + case "$service_id" in + jellyfin) ctl="jellyfinctl" ;; + lyrion) ctl="lyrionctl" ;; + jitsi) ctl="jitsctl" ;; + peertube) ctl="peertubectl" ;; + streamlit) ctl="streamlitctl" ;; + magicmirror) ctl="mmctl" ;; + gotosocial) ctl="gotosocialctl" ;; + esac + + if [ -z "$ctl" ] || ! command -v "$ctl" >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "Service not installed or unknown" + json_dump + return + fi + + local output=$("$ctl" restart 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Service restarted" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# =========================================== +# Main Dispatcher +# =========================================== + +case "$1" in + list) + cat << 'EOF' +{ + "status": {}, + "services": {}, + "service_status": {"id": "str"}, + "service_start": {"id": "str"}, + "service_stop": {"id": "str"}, + "service_restart": {"id": "str"} +} +EOF + ;; + call) + case "$2" in + status) get_status ;; + services) get_services ;; + service_status) get_service_status ;; + service_start) service_start ;; + service_stop) service_stop ;; + service_restart) service_restart ;; + *) echo '{"error": "Unknown method"}' ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-media-hub/root/usr/share/luci/menu.d/luci-app-media-hub.json b/package/secubox/luci-app-media-hub/root/usr/share/luci/menu.d/luci-app-media-hub.json new file mode 100644 index 00000000..fdc60484 --- /dev/null +++ b/package/secubox/luci-app-media-hub/root/usr/share/luci/menu.d/luci-app-media-hub.json @@ -0,0 +1,13 @@ +{ + "admin/services/media-hub": { + "title": "Media Hub", + "order": 10, + "action": { + "type": "view", + "path": "media-hub/dashboard" + }, + "depends": { + "acl": ["luci-app-media-hub"] + } + } +} diff --git a/package/secubox/luci-app-media-hub/root/usr/share/rpcd/acl.d/luci-app-media-hub.json b/package/secubox/luci-app-media-hub/root/usr/share/rpcd/acl.d/luci-app-media-hub.json new file mode 100644 index 00000000..fd2fc99e --- /dev/null +++ b/package/secubox/luci-app-media-hub/root/usr/share/rpcd/acl.d/luci-app-media-hub.json @@ -0,0 +1,23 @@ +{ + "luci-app-media-hub": { + "description": "Grant access to Media Hub dashboard", + "read": { + "ubus": { + "luci.media-hub": [ + "status", + "services", + "service_status" + ] + } + }, + "write": { + "ubus": { + "luci.media-hub": [ + "service_start", + "service_stop", + "service_restart" + ] + } + } + } +} diff --git a/package/secubox/luci-app-saas-relay/Makefile b/package/secubox/luci-app-saas-relay/Makefile new file mode 100644 index 00000000..784073ad --- /dev/null +++ b/package/secubox/luci-app-saas-relay/Makefile @@ -0,0 +1,29 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-saas-relay +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI SaaS Relay Dashboard +LUCI_DEPENDS:=+secubox-app-saas-relay +luci-base + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-saas-relay/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.saas-relay $(1)/usr/libexec/rpcd/luci.saas-relay + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-saas-relay.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-saas-relay.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/saas-relay + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/saas-relay/*.js $(1)/www/luci-static/resources/view/saas-relay/ +endef + +$(eval $(call BuildPackage,luci-app-saas-relay)) diff --git a/package/secubox/luci-app-saas-relay/htdocs/luci-static/resources/view/saas-relay/overview.js b/package/secubox/luci-app-saas-relay/htdocs/luci-static/resources/view/saas-relay/overview.js new file mode 100644 index 00000000..dc7c2487 --- /dev/null +++ b/package/secubox/luci-app-saas-relay/htdocs/luci-static/resources/view/saas-relay/overview.js @@ -0,0 +1,457 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.saas-relay', + method: 'status', + expect: {} +}); + +var callListServices = rpc.declare({ + object: 'luci.saas-relay', + method: 'list_services', + expect: {} +}); + +var callServiceEnable = rpc.declare({ + object: 'luci.saas-relay', + method: 'service_enable', + params: ['id'], + expect: {} +}); + +var callServiceDisable = rpc.declare({ + object: 'luci.saas-relay', + method: 'service_disable', + params: ['id'], + expect: {} +}); + +var callServiceAdd = rpc.declare({ + object: 'luci.saas-relay', + method: 'service_add', + params: ['id', 'name', 'domain', 'emoji'], + expect: {} +}); + +var callServiceDelete = rpc.declare({ + object: 'luci.saas-relay', + method: 'service_delete', + params: ['id'], + expect: {} +}); + +var callStart = rpc.declare({ + object: 'luci.saas-relay', + method: 'start', + expect: {} +}); + +var callStop = rpc.declare({ + object: 'luci.saas-relay', + method: 'stop', + expect: {} +}); + +var callSetup = rpc.declare({ + object: 'luci.saas-relay', + method: 'setup', + expect: {} +}); + +var callListCookies = rpc.declare({ + object: 'luci.saas-relay', + method: 'list_cookies', + params: ['service'], + expect: {} +}); + +var callImportCookies = rpc.declare({ + object: 'luci.saas-relay', + method: 'import_cookies', + params: ['service', 'cookies'], + expect: {} +}); + +var callClearCookies = rpc.declare({ + object: 'luci.saas-relay', + method: 'clear_cookies', + params: ['service'], + expect: {} +}); + +var callGetLog = rpc.declare({ + object: 'luci.saas-relay', + method: 'get_log', + params: ['lines'], + expect: {} +}); + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callListServices() + ]); + }, + + renderStatusCard: function(status) { + var statusEmoji = status.status === 'running' ? '✅' : '⏸️'; + var enabledEmoji = status.enabled ? '🔓' : '🔐'; + + return E('div', { 'class': 'cbi-section', 'style': 'background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #e0e0e0; border-radius: 12px; padding: 20px; margin-bottom: 20px;' }, [ + E('h3', { 'style': 'color: #f97316; margin-bottom: 15px;' }, '🔄 SaaS Relay Status'), + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;' }, [ + E('div', { 'class': 'stat-box', 'style': 'background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 2em;' }, statusEmoji), + E('div', { 'style': 'font-size: 0.9em; color: #999;' }, 'Status'), + E('div', { 'style': 'font-weight: bold;' }, status.status || 'Unknown') + ]), + E('div', { 'class': 'stat-box', 'style': 'background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 2em;' }, '🔗'), + E('div', { 'style': 'font-size: 0.9em; color: #999;' }, 'Services'), + E('div', { 'style': 'font-weight: bold;' }, (status.enabled_services || 0) + ' / ' + (status.service_count || 0)) + ]), + E('div', { 'class': 'stat-box', 'style': 'background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 2em;' }, '🍪'), + E('div', { 'style': 'font-size: 0.9em; color: #999;' }, 'Cookies'), + E('div', { 'style': 'font-weight: bold;' }, status.total_cookies || 0) + ]), + E('div', { 'class': 'stat-box', 'style': 'background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 2em;' }, '🌐'), + E('div', { 'style': 'font-size: 0.9em; color: #999;' }, 'Port'), + E('div', { 'style': 'font-weight: bold;' }, status.proxy_port || 8890) + ]) + ]) + ]); + }, + + renderServiceCard: function(service) { + var self = this; + var statusEmoji = service.enabled ? '🟢' : '🔴'; + var cookieEmoji = service.cookie_count > 0 ? '🍪' : '⚪'; + + var card = E('div', { + 'class': 'service-card', + 'style': 'background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 15px; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between;' + }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: 15px;' }, [ + E('span', { 'style': 'font-size: 2em;' }, service.emoji || '🔗'), + E('div', {}, [ + E('div', { 'style': 'font-weight: bold; font-size: 1.1em;' }, [ + statusEmoji, ' ', service.name + ]), + E('div', { 'style': 'color: #666; font-size: 0.9em;' }, service.domain), + E('div', { 'style': 'color: #999; font-size: 0.8em;' }, [ + cookieEmoji, ' ', service.cookie_count, ' cookies' + ]) + ]) + ]), + E('div', { 'style': 'display: flex; gap: 8px;' }, [ + E('button', { + 'class': 'cbi-button', + 'style': 'padding: 5px 10px;', + 'click': function() { self.toggleService(service); } + }, service.enabled ? '🔐 Disable' : '🔓 Enable'), + E('button', { + 'class': 'cbi-button', + 'style': 'padding: 5px 10px;', + 'click': function() { self.manageCookies(service); } + }, '🍪 Cookies'), + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'style': 'padding: 5px 10px;', + 'click': function() { self.deleteService(service.id); } + }, '🗑️') + ]) + ]); + + return card; + }, + + toggleService: function(service) { + var self = this; + var action = service.enabled ? callServiceDisable : callServiceEnable; + + action({ id: service.id }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', result.emoji + ' ' + result.message), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', '❌ ' + (result.error || 'Failed')), 'error'); + } + }); + }, + + deleteService: function(id) { + var self = this; + if (!confirm('🗑️ Delete service "' + id + '" and its cookies?')) return; + + callServiceDelete({ id: id }).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', result.emoji + ' ' + result.message), 'success'); + self.refreshView(); + } + }); + }, + + manageCookies: function(service) { + var self = this; + + callListCookies({ service: service.id }).then(function(result) { + var content = E('div', {}, [ + E('h4', {}, '🍪 Cookies for ' + service.name), + E('p', {}, 'Current: ' + (result.count || 0) + ' cookies'), + E('hr'), + E('h5', {}, 'Import Cookies (JSON format)'), + E('textarea', { + 'id': 'cookie-import-text', + 'style': 'width: 100%; height: 150px; font-family: monospace;', + 'placeholder': '{"cookie_name": "cookie_value", ...}' + }, result.cookies !== '{}' ? result.cookies : ''), + E('div', { 'style': 'margin-top: 10px; display: flex; gap: 10px;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + var cookies = document.getElementById('cookie-import-text').value; + try { + JSON.parse(cookies); + callImportCookies({ service: service.id, cookies: cookies }).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', res.emoji + ' ' + res.message), 'success'); + ui.hideModal(); + } + }); + } catch (e) { + ui.addNotification(null, E('p', '❌ Invalid JSON'), 'error'); + } + } + }, '📥 Import'), + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': function() { + if (confirm('Clear all cookies for ' + service.name + '?')) { + callClearCookies({ service: service.id }).then(function(res) { + ui.addNotification(null, E('p', res.emoji + ' ' + res.message), 'success'); + ui.hideModal(); + }); + } + } + }, '🗑️ Clear'), + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Close') + ]) + ]); + + ui.showModal('Cookie Manager', content); + }); + }, + + addService: function() { + var self = this; + + var content = E('div', {}, [ + E('h4', {}, '➕ Add New Service'), + E('div', { 'style': 'display: grid; gap: 10px;' }, [ + E('label', {}, [ + 'ID (lowercase, no spaces):', + E('input', { 'type': 'text', 'id': 'new-svc-id', 'style': 'width: 100%; padding: 5px;' }) + ]), + E('label', {}, [ + 'Name:', + E('input', { 'type': 'text', 'id': 'new-svc-name', 'style': 'width: 100%; padding: 5px;' }) + ]), + E('label', {}, [ + 'Domain:', + E('input', { 'type': 'text', 'id': 'new-svc-domain', 'style': 'width: 100%; padding: 5px;', 'placeholder': 'example.com' }) + ]), + E('label', {}, [ + 'Emoji:', + E('input', { 'type': 'text', 'id': 'new-svc-emoji', 'style': 'width: 100%; padding: 5px;', 'placeholder': '🔗', 'value': '🔗' }) + ]) + ]), + E('div', { 'style': 'margin-top: 15px; display: flex; gap: 10px;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + var id = document.getElementById('new-svc-id').value; + var name = document.getElementById('new-svc-name').value; + var domain = document.getElementById('new-svc-domain').value; + var emoji = document.getElementById('new-svc-emoji').value || '🔗'; + + if (!id || !name || !domain) { + ui.addNotification(null, E('p', '❌ All fields required'), 'error'); + return; + } + + callServiceAdd({ id: id, name: name, domain: domain, emoji: emoji }).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', res.emoji + ' ' + res.message), 'success'); + ui.hideModal(); + self.refreshView(); + } else { + ui.addNotification(null, E('p', '❌ ' + res.error), 'error'); + } + }); + } + }, '➕ Add Service'), + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Cancel') + ]) + ]); + + ui.showModal('Add Service', content); + }, + + startRelay: function() { + var self = this; + callStart().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', result.emoji + ' ' + result.message), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', '❌ ' + result.error), 'error'); + } + }); + }, + + stopRelay: function() { + var self = this; + callStop().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', result.emoji + ' ' + result.message), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', '❌ ' + result.error), 'error'); + } + }); + }, + + setupRelay: function() { + var self = this; + callSetup().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', result.emoji + ' ' + result.message), 'success'); + self.refreshView(); + } else { + ui.addNotification(null, E('p', '❌ ' + result.error), 'error'); + } + }); + }, + + showLog: function() { + callGetLog({ lines: 50 }).then(function(result) { + var entries = result.entries || []; + var logContent = entries.length > 0 ? entries.join('\n') : 'No activity logged'; + + var content = E('div', {}, [ + E('h4', {}, '📋 Activity Log'), + E('pre', { + 'style': 'background: #1a1a2e; color: #e0e0e0; padding: 15px; border-radius: 8px; max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 0.85em;' + }, logContent), + E('div', { 'style': 'margin-top: 10px;' }, [ + E('button', { + 'class': 'cbi-button', + 'click': ui.hideModal + }, 'Close') + ]) + ]); + + ui.showModal('Activity Log', content); + }); + }, + + refreshView: function() { + var self = this; + Promise.all([callStatus(), callListServices()]).then(function(results) { + var status = results[0]; + var services = results[1].services || []; + + // Update status card + var statusContainer = document.getElementById('status-container'); + statusContainer.innerHTML = ''; + statusContainer.appendChild(self.renderStatusCard(status)); + + // Update services + var servicesContainer = document.getElementById('services-container'); + servicesContainer.innerHTML = ''; + services.forEach(function(svc) { + servicesContainer.appendChild(self.renderServiceCard(svc)); + }); + }); + }, + + render: function(data) { + var self = this; + var status = data[0] || {}; + var services = (data[1] && data[1].services) || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'class': 'cbi-map-title' }, '🔄 SaaS Relay'), + E('div', { 'class': 'cbi-map-descr' }, + 'Shared browser session proxy for team access to external SaaS services. Uses SecuBox authentication with mitmproxy cookie injection.'), + + // Control buttons + E('div', { 'style': 'margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.startRelay(); } + }, '▶️ Start'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.stopRelay(); } + }, '⏹️ Stop'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.setupRelay(); } + }, '⚙️ Setup'), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { self.addService(); } + }, '➕ Add Service'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.showLog(); } + }, '📋 View Log'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.refreshView(); } + }, '🔄 Refresh') + ]), + + // Status card container + E('div', { 'id': 'status-container' }), + + // Services section + E('h3', { 'style': 'margin-top: 20px;' }, '🔗 Connected Services'), + E('div', { 'id': 'services-container' }) + ]); + + // Render initial content + var statusContainer = view.querySelector('#status-container'); + statusContainer.appendChild(this.renderStatusCard(status)); + + var servicesContainer = view.querySelector('#services-container'); + if (services.length === 0) { + servicesContainer.appendChild(E('p', { 'style': 'color: #666; text-align: center; padding: 20px;' }, + 'No services configured. Click "➕ Add Service" to get started.')); + } else { + services.forEach(function(svc) { + servicesContainer.appendChild(self.renderServiceCard(svc)); + }); + } + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-saas-relay/root/usr/libexec/rpcd/luci.saas-relay b/package/secubox/luci-app-saas-relay/root/usr/libexec/rpcd/luci.saas-relay new file mode 100755 index 00000000..f9947aa7 --- /dev/null +++ b/package/secubox/luci-app-saas-relay/root/usr/libexec/rpcd/luci.saas-relay @@ -0,0 +1,461 @@ +#!/bin/sh +# SaaS Relay RPCD backend + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONFIG="saas-relay" +SAASCTL="/usr/sbin/saasctl" +DATA_PATH="/srv/saas-relay" +COOKIES_PATH="$DATA_PATH/cookies" +LOG_PATH="$DATA_PATH/logs" + +uci_get() { uci -q get ${CONFIG}.$1; } + +# =========================================== +# Status Methods +# =========================================== + +get_status() { + json_init + + local enabled=$(uci_get main.enabled) || enabled="0" + local status=$(uci_get main.status) || status="stopped" + local proxy_port=$(uci_get main.proxy_port) || proxy_port="8890" + + json_add_boolean "enabled" "$enabled" + json_add_string "status" "$status" + json_add_int "proxy_port" "$proxy_port" + json_add_string "data_path" "$DATA_PATH" + + # Count services and cookies + local service_count=0 + local cookie_count=0 + local enabled_services=0 + + config_load "$CONFIG" + _count_service() { + local section="$1" + local svc_enabled domain + config_get domain "$section" domain + [ -z "$domain" ] && return + + service_count=$((service_count + 1)) + config_get svc_enabled "$section" enabled "0" + [ "$svc_enabled" = "1" ] && enabled_services=$((enabled_services + 1)) + } + config_foreach _count_service service + + for f in "$COOKIES_PATH"/*.json; do + [ -f "$f" ] || continue + local count=$(grep -c '"' "$f" 2>/dev/null | awk '{print int($1/2)}') + cookie_count=$((cookie_count + count)) + done + + json_add_int "service_count" "$service_count" + json_add_int "enabled_services" "$enabled_services" + json_add_int "total_cookies" "$cookie_count" + + json_dump +} + +# =========================================== +# Service Methods +# =========================================== + +list_services() { + json_init + json_add_array "services" + + config_load "$CONFIG" + + _add_service() { + local section="$1" + local enabled name emoji domain status cookie_domains auth_required last_check + + config_get domain "$section" domain + [ -z "$domain" ] && return + + config_get enabled "$section" enabled "0" + config_get name "$section" name "$section" + config_get emoji "$section" emoji "🔗" + config_get status "$section" status "disconnected" + config_get cookie_domains "$section" cookie_domains "$domain" + config_get auth_required "$section" auth_required "1" + config_get last_check "$section" last_check "0" + + # Count cookies for this service + local cookie_file="$COOKIES_PATH/${section}.json" + local cookie_count=0 + [ -f "$cookie_file" ] && cookie_count=$(grep -c '"' "$cookie_file" 2>/dev/null | awk '{print int($1/2)}') + + json_add_object + json_add_string "id" "$section" + json_add_string "name" "$name" + json_add_string "emoji" "$emoji" + json_add_string "domain" "$domain" + json_add_boolean "enabled" "$enabled" + json_add_string "status" "$status" + json_add_int "cookie_count" "$cookie_count" + json_add_string "cookie_domains" "$cookie_domains" + json_add_boolean "auth_required" "$auth_required" + json_add_int "last_check" "$last_check" + json_close_object + } + + config_foreach _add_service service + + json_close_array + json_dump +} + +service_enable() { + read input + json_load "$input" + json_get_var id id + + json_init + + if [ -z "$id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Service ID required" + json_dump + return + fi + + uci set ${CONFIG}.${id}.enabled='1' + uci commit ${CONFIG} + + json_add_boolean "success" 1 + json_add_string "message" "Service enabled" + json_add_string "emoji" "🔓" + + json_dump +} + +service_disable() { + read input + json_load "$input" + json_get_var id id + + json_init + + if [ -z "$id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Service ID required" + json_dump + return + fi + + uci set ${CONFIG}.${id}.enabled='0' + uci commit ${CONFIG} + + json_add_boolean "success" 1 + json_add_string "message" "Service disabled" + json_add_string "emoji" "🔐" + + json_dump +} + +service_add() { + read input + json_load "$input" + json_get_var id id + json_get_var name name + json_get_var domain domain + json_get_var emoji emoji + + json_init + + if [ -z "$id" ] || [ -z "$name" ] || [ -z "$domain" ]; then + json_add_boolean "success" 0 + json_add_string "error" "ID, name, and domain required" + json_dump + return + fi + + [ -z "$emoji" ] && emoji="🔗" + + uci set ${CONFIG}.${id}=service + uci set ${CONFIG}.${id}.enabled='1' + uci set ${CONFIG}.${id}.name="$name" + uci set ${CONFIG}.${id}.emoji="$emoji" + uci set ${CONFIG}.${id}.domain="$domain" + uci set ${CONFIG}.${id}.cookie_domains="$domain,.$domain" + uci set ${CONFIG}.${id}.auth_required='1' + uci set ${CONFIG}.${id}.status='disconnected' + uci commit ${CONFIG} + + json_add_boolean "success" 1 + json_add_string "message" "Service added: $name" + json_add_string "emoji" "$emoji" + + json_dump +} + +service_delete() { + read input + json_load "$input" + json_get_var id id + + json_init + + if [ -z "$id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Service ID required" + json_dump + return + fi + + uci delete ${CONFIG}.${id} 2>/dev/null + uci commit ${CONFIG} + + # Remove cookies + rm -f "$COOKIES_PATH/${id}.json" + + json_add_boolean "success" 1 + json_add_string "message" "Service deleted" + json_add_string "emoji" "🗑️" + + json_dump +} + +# =========================================== +# Cookie Methods +# =========================================== + +list_cookies() { + read input + json_load "$input" + json_get_var service service + + json_init + json_add_boolean "success" 1 + + if [ -n "$service" ]; then + local cookie_file="$COOKIES_PATH/${service}.json" + json_add_string "service" "$service" + + if [ -f "$cookie_file" ]; then + local content=$(cat "$cookie_file") + json_add_string "cookies" "$content" + local count=$(grep -c '"' "$cookie_file" 2>/dev/null | awk '{print int($1/2)}') + json_add_int "count" "$count" + else + json_add_string "cookies" "{}" + json_add_int "count" 0 + fi + else + json_add_array "services" + + for f in "$COOKIES_PATH"/*.json; do + [ -f "$f" ] || continue + local svc=$(basename "$f" .json) + local count=$(grep -c '"' "$f" 2>/dev/null | awk '{print int($1/2)}') + local size=$(stat -c%s "$f" 2>/dev/null || echo 0) + local modified=$(stat -c%Y "$f" 2>/dev/null || echo 0) + + json_add_object + json_add_string "service" "$svc" + json_add_int "count" "$count" + json_add_int "size" "$size" + json_add_int "modified" "$modified" + json_close_object + done + + json_close_array + fi + + json_dump +} + +import_cookies() { + read input + json_load "$input" + json_get_var service service + json_get_var cookies cookies + + json_init + + if [ -z "$service" ] || [ -z "$cookies" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Service and cookies required" + json_dump + return + fi + + mkdir -p "$COOKIES_PATH" + echo "$cookies" > "$COOKIES_PATH/${service}.json" + chmod 600 "$COOKIES_PATH/${service}.json" + + local count=$(grep -c '"' "$COOKIES_PATH/${service}.json" 2>/dev/null | awk '{print int($1/2)}') + + json_add_boolean "success" 1 + json_add_string "message" "Imported $count cookies" + json_add_string "emoji" "🍪" + json_add_int "count" "$count" + + json_dump +} + +clear_cookies() { + read input + json_load "$input" + json_get_var service service + + json_init + + if [ -n "$service" ]; then + rm -f "$COOKIES_PATH/${service}.json" + json_add_string "message" "Cleared cookies for $service" + else + rm -f "$COOKIES_PATH"/*.json + json_add_string "message" "Cleared all cookies" + fi + + json_add_boolean "success" 1 + json_add_string "emoji" "🗑️" + + json_dump +} + +# =========================================== +# Control Methods +# =========================================== + +start_relay() { + json_init + + local output=$("$SAASCTL" start 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "SaaS Relay started" + json_add_string "emoji" "🔄" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +stop_relay() { + json_init + + local output=$("$SAASCTL" stop 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "SaaS Relay stopped" + json_add_string "emoji" "🔌" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +setup_relay() { + json_init + + local output=$("$SAASCTL" setup 2>&1) + local result=$? + + if [ "$result" -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Setup complete" + json_add_string "emoji" "✅" + else + json_add_boolean "success" 0 + json_add_string "error" "$output" + fi + + json_dump +} + +# =========================================== +# Activity Log +# =========================================== + +get_log() { + read input + json_load "$input" + json_get_var lines lines + + [ -z "$lines" ] && lines=50 + + json_init + json_add_boolean "success" 1 + json_add_array "entries" + + if [ -f "$LOG_PATH/activity.log" ]; then + tail -n "$lines" "$LOG_PATH/activity.log" | while read line; do + json_add_string "" "$line" + done + fi + + json_close_array + json_dump +} + +clear_log() { + json_init + + > "$LOG_PATH/activity.log" 2>/dev/null + + json_add_boolean "success" 1 + json_add_string "message" "Log cleared" + json_add_string "emoji" "📋" + + json_dump +} + +# =========================================== +# Main Dispatcher +# =========================================== + +case "$1" in + list) + cat << 'EOF' +{ + "status": {}, + "list_services": {}, + "service_enable": {"id": "str"}, + "service_disable": {"id": "str"}, + "service_add": {"id": "str", "name": "str", "domain": "str", "emoji": "str"}, + "service_delete": {"id": "str"}, + "list_cookies": {"service": "str"}, + "import_cookies": {"service": "str", "cookies": "str"}, + "clear_cookies": {"service": "str"}, + "start": {}, + "stop": {}, + "setup": {}, + "get_log": {"lines": "int"}, + "clear_log": {} +} +EOF + ;; + call) + case "$2" in + status) get_status ;; + list_services) list_services ;; + service_enable) service_enable ;; + service_disable) service_disable ;; + service_add) service_add ;; + service_delete) service_delete ;; + list_cookies) list_cookies ;; + import_cookies) import_cookies ;; + clear_cookies) clear_cookies ;; + start) start_relay ;; + stop) stop_relay ;; + setup) setup_relay ;; + get_log) get_log ;; + clear_log) clear_log ;; + *) echo '{"error": "Unknown method"}' ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-saas-relay/root/usr/share/luci/menu.d/luci-app-saas-relay.json b/package/secubox/luci-app-saas-relay/root/usr/share/luci/menu.d/luci-app-saas-relay.json new file mode 100644 index 00000000..d212e2bf --- /dev/null +++ b/package/secubox/luci-app-saas-relay/root/usr/share/luci/menu.d/luci-app-saas-relay.json @@ -0,0 +1,14 @@ +{ + "admin/services/saas-relay": { + "title": "SaaS Relay", + "order": 85, + "action": { + "type": "view", + "path": "saas-relay/overview" + }, + "depends": { + "acl": ["luci-app-saas-relay"], + "uci": { "saas-relay": true } + } + } +} diff --git a/package/secubox/luci-app-saas-relay/root/usr/share/rpcd/acl.d/luci-app-saas-relay.json b/package/secubox/luci-app-saas-relay/root/usr/share/rpcd/acl.d/luci-app-saas-relay.json new file mode 100644 index 00000000..02538884 --- /dev/null +++ b/package/secubox/luci-app-saas-relay/root/usr/share/rpcd/acl.d/luci-app-saas-relay.json @@ -0,0 +1,33 @@ +{ + "luci-app-saas-relay": { + "description": "Grant access to SaaS Relay", + "read": { + "ubus": { + "luci.saas-relay": [ + "status", + "list_services", + "list_cookies", + "get_log" + ] + }, + "uci": ["saas-relay"] + }, + "write": { + "ubus": { + "luci.saas-relay": [ + "service_enable", + "service_disable", + "service_add", + "service_delete", + "import_cookies", + "clear_cookies", + "start", + "stop", + "setup", + "clear_log" + ] + }, + "uci": ["saas-relay"] + } + } +} diff --git a/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js b/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js index 7ebde7ad..b9d574e3 100644 --- a/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js +++ b/package/secubox/luci-app-secubox-users/htdocs/luci-static/resources/view/secubox-users/overview.js @@ -60,11 +60,24 @@ return view.extend({ }, s); }); + // Login stats + var lastLogin = user.last_login || 'never'; + var loginSuccess = user.login_success || 0; + var loginFailure = user.login_failure || 0; + + // Color code failures + var failColor = loginFailure > 10 ? '#f44336' : (loginFailure > 0 ? '#ff9800' : '#4caf50'); + return E('tr', {}, [ E('td', {}, user.username), E('td', {}, user.email), + E('td', {}, lastLogin), + E('td', { 'style': 'text-align:center;' }, [ + E('span', { 'style': 'color:#4caf50;font-weight:bold;' }, String(loginSuccess)), + E('span', {}, ' / '), + E('span', { 'style': 'color:' + failColor + ';font-weight:bold;' }, String(loginFailure)) + ]), E('td', {}, services), - E('td', {}, user.enabled === '1' ? 'Yes' : 'No'), E('td', {}, [ E('button', { 'class': 'btn cbi-button', @@ -271,8 +284,9 @@ return view.extend({ E('tr', { 'class': 'tr table-titles' }, [ E('th', { 'class': 'th' }, _('Username')), E('th', { 'class': 'th' }, _('Email')), + E('th', { 'class': 'th' }, _('Last Login')), + E('th', { 'class': 'th', 'style': 'text-align:center;' }, _('OK / Fail')), E('th', { 'class': 'th' }, _('Services')), - E('th', { 'class': 'th' }, _('Enabled')), E('th', { 'class': 'th' }, _('Actions')) ]) ].concat(userRows)) : diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages b/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages index 1923c6a8..c047c724 100644 --- a/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages +++ b/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages @@ -11,7 +11,7 @@ Description: Unified AI security insights dashboard for SecuBox. DNS Guard, Network Anomaly, CVE Triage, and LocalRecall. Provides security posture scoring and AI-powered analysis. Filename: luci-app-ai-insights_1.0.0-r1_all.ipk -Size: 11638 +Size: 11639 Package: luci-app-auth-guardian Version: 0.4.0-r3 @@ -23,7 +23,7 @@ Architecture: all Installed-Size: 71680 Description: Comprehensive authentication and session management with captive portal, OAuth2/OIDC integration, voucher system, and time-based access control Filename: luci-app-auth-guardian_0.4.0-r3_all.ipk -Size: 12400 +Size: 12391 Package: luci-app-backup Version: 1.0.0-r1 @@ -35,7 +35,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI Backup Manager Filename: luci-app-backup_1.0.0-r1_all.ipk -Size: 4534 +Size: 4537 Package: luci-app-bandwidth-manager Version: 0.5.0-r2 @@ -59,7 +59,7 @@ Architecture: all Installed-Size: 122880 Description: Dashboard for managing local CDN caching proxy on OpenWrt Filename: luci-app-cdn-cache_0.5.0-r3_all.ipk -Size: 24255 +Size: 24257 Package: luci-app-client-guardian Version: 0.4.0-r7 @@ -71,7 +71,7 @@ Architecture: all Installed-Size: 276480 Description: Network Access Control with client monitoring, zone management, captive portal, parental controls, and SMS/email alerts Filename: luci-app-client-guardian_0.4.0-r7_all.ipk -Size: 52684 +Size: 52687 Package: luci-app-cloner Version: 1.0.0-r1 @@ -82,7 +82,7 @@ Architecture: all Installed-Size: 102400 Description: SecuBox cloning station for building and deploying clone images Filename: luci-app-cloner_1.0.0-r1_all.ipk -Size: 19433 +Size: 19435 Package: luci-app-config-advisor Version: 1.0.0-r1 @@ -105,7 +105,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI Cookie Tracker Dashboard Filename: luci-app-cookie-tracker_1.0.0-r1_all.ipk -Size: 5653 +Size: 5660 Package: luci-app-crowdsec-dashboard Version: 0.7.0-r32 @@ -117,7 +117,7 @@ Architecture: all Installed-Size: 184320 Description: Real-time security monitoring dashboard for CrowdSec on OpenWrt Filename: luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk -Size: 34938 +Size: 34940 Package: luci-app-cve-triage Version: 1.0.0-r1 @@ -129,7 +129,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI CVE Triage Dashboard Filename: luci-app-cve-triage_1.0.0-r1_all.ipk -Size: 5948 +Size: 5943 Package: luci-app-cyberfeed Version: 0.1.1-r1 @@ -153,7 +153,7 @@ Architecture: all Installed-Size: 71680 Description: LuCI SecuBox Device Intelligence Filename: luci-app-device-intel_1.0.0-r1_all.ipk -Size: 12047 +Size: 12049 Package: luci-app-dnsguard Version: 1.1.0-r1 @@ -172,7 +172,7 @@ Description: SecuBox DNS Guard provides privacy-focused DNS management with AI- - Real-time alerts and blocklist management - Domain analysis with LocalAI integration Filename: luci-app-dnsguard_1.1.0-r1_all.ipk -Size: 12448 +Size: 12449 Package: luci-app-dns-provider Version: 1.0.0-r1 @@ -184,7 +184,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI DNS Provider Manager Filename: luci-app-dns-provider_1.0.0-r1_all.ipk -Size: 7168 +Size: 7174 Package: luci-app-domoticz Version: 1.0.0-r1 @@ -196,7 +196,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI Domoticz Home Automation Configuration Filename: luci-app-domoticz_1.0.0-r1_all.ipk -Size: 7123 +Size: 7118 Package: luci-app-exposure Version: 1.0.0-r3 @@ -208,7 +208,7 @@ Architecture: all Installed-Size: 71680 Description: LuCI SecuBox Service Exposure Manager Filename: luci-app-exposure_1.0.0-r3_all.ipk -Size: 11696 +Size: 11698 Package: luci-app-gitea Version: 1.0.0-r2 @@ -220,7 +220,7 @@ Architecture: all Installed-Size: 92160 Description: Modern dashboard for Gitea Platform management on OpenWrt Filename: luci-app-gitea_1.0.0-r2_all.ipk -Size: 16623 +Size: 16621 Package: luci-app-glances Version: 1.0.0-r2 @@ -232,7 +232,7 @@ Architecture: all Installed-Size: 51200 Description: Modern dashboard for Glances system monitoring with SecuBox theme Filename: luci-app-glances_1.0.0-r2_all.ipk -Size: 7022 +Size: 7017 Package: luci-app-gotosocial Version: 0.1.0-r1 @@ -244,7 +244,7 @@ Architecture: all Installed-Size: 51200 Description: LuCI app for GoToSocial Fediverse Server Filename: luci-app-gotosocial_0.1.0-r1_all.ipk -Size: 8207 +Size: 8210 Package: luci-app-haproxy Version: 1.0.0-r8 @@ -268,7 +268,7 @@ Architecture: all Installed-Size: 194560 Description: Modern dashboard for Hexo static site generator on OpenWrt Filename: luci-app-hexojs_1.0.0-r3_all.ipk -Size: 30452 +Size: 30450 Package: luci-app-iot-guard Version: 1.0.0-r1 @@ -280,7 +280,7 @@ Architecture: all Installed-Size: 61440 Description: IoT device isolation and security monitoring interface Filename: luci-app-iot-guard_1.0.0-r1_all.ipk -Size: 10535 +Size: 10538 Package: luci-app-jabber Version: 0 @@ -292,7 +292,7 @@ Architecture: all Installed-Size: 61440 Description: LuCI Jabber/XMPP Server (Prosody) Filename: luci-app-jabber_0_all.ipk -Size: 9304 +Size: 9308 Package: luci-app-jellyfin Version: 1.0.0-r1 @@ -304,7 +304,7 @@ Architecture: all Installed-Size: 51200 Description: LuCI Jellyfin Media Server Configuration Filename: luci-app-jellyfin_1.0.0-r1_all.ipk -Size: 10482 +Size: 10485 Package: luci-app-jitsi Version: 1.0.0-r1 @@ -316,7 +316,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI Jitsi Meet Configuration Filename: luci-app-jitsi_1.0.0-r1_all.ipk -Size: 5175 +Size: 5173 Package: luci-app-ksm-manager Version: 0.4.0-r2 @@ -328,7 +328,7 @@ Architecture: all Installed-Size: 112640 Description: Centralized cryptographic key management with hardware security module (HSM) support for Nitrokey and YubiKey devices. Provides secure key storage, certificate management, SSH key handling, and secret storage with audit logging. Filename: luci-app-ksm-manager_0.4.0-r2_all.ipk -Size: 18779 +Size: 18777 Package: luci-app-localai Version: 0.1.0-r15 @@ -352,7 +352,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI LocalRecall AI Memory Dashboard Filename: luci-app-localrecall_1.0.0-r1_all.ipk -Size: 8418 +Size: 8420 Package: luci-app-lyrion Version: 1.0.0-r1 @@ -364,7 +364,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI support for Lyrion Music Server Filename: luci-app-lyrion_1.0.0-r1_all.ipk -Size: 6839 +Size: 6840 Package: luci-app-mac-guardian Version: 0.5.0-r1 @@ -376,7 +376,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI MAC Guardian - WiFi MAC Security Monitor Filename: luci-app-mac-guardian_0.5.0-r1_all.ipk -Size: 6664 +Size: 6660 Package: luci-app-magicmirror2 Version: 0.4.0-r6 @@ -388,7 +388,7 @@ Architecture: all Installed-Size: 71680 Description: Modern dashboard for MagicMirror2 smart display platform with module manager and SecuBox theme Filename: luci-app-magicmirror2_0.4.0-r6_all.ipk -Size: 12361 +Size: 12358 Package: luci-app-mailinabox Version: 1.0.0-r1 @@ -400,7 +400,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI support for Mail-in-a-Box Filename: luci-app-mailinabox_1.0.0-r1_all.ipk -Size: 5484 +Size: 5487 Package: luci-app-mailserver Version: 1.0.0-r1 @@ -412,7 +412,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI Mail Server Manager Filename: luci-app-mailserver_1.0.0-r1_all.ipk -Size: 6511 +Size: 6509 Package: luci-app-master-link Version: 1.0.0-r1 @@ -424,7 +424,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI SecuBox Master-Link Mesh Management Filename: luci-app-master-link_1.0.0-r1_all.ipk -Size: 6304 +Size: 6309 Package: luci-app-matrix Version: 1.0.0-r1 @@ -448,7 +448,7 @@ Architecture: all Installed-Size: 133120 Description: Real-time detection and monitoring of streaming services (Netflix, YouTube, Spotify, etc.) with quality estimation, history tracking, and alerts. Supports nDPId local DPI and netifyd. Filename: luci-app-media-flow_0.6.4-r1_all.ipk -Size: 25381 +Size: 25379 Package: luci-app-metablogizer Version: 1.1.0-r1 @@ -460,7 +460,7 @@ Architecture: all Installed-Size: 133120 Description: LuCI support for MetaBlogizer Static Site Publisher Filename: luci-app-metablogizer_1.1.0-r1_all.ipk -Size: 26207 +Size: 26203 Package: luci-app-metabolizer Version: 1.0.0-r2 @@ -472,7 +472,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI support for Metabolizer CMS Filename: luci-app-metabolizer_1.0.0-r2_all.ipk -Size: 4821 +Size: 4819 Package: luci-app-mitmproxy Version: 0.5.0-r2 @@ -484,7 +484,7 @@ Architecture: all Installed-Size: 71680 Description: Modern dashboard for mitmproxy HTTPS traffic inspection with SecuBox theme Filename: luci-app-mitmproxy_0.5.0-r2_all.ipk -Size: 13232 +Size: 13234 Package: luci-app-mmpm Version: 0.2.0-r3 @@ -496,7 +496,7 @@ Architecture: all Installed-Size: 51200 Description: Web interface for MMPM - MagicMirror Package Manager Filename: luci-app-mmpm_0.2.0-r3_all.ipk -Size: 7975 +Size: 7971 Package: luci-app-mqtt-bridge Version: 0.4.0-r4 @@ -508,7 +508,7 @@ Architecture: all Installed-Size: 122880 Description: USB-to-MQTT IoT hub with SecuBox theme Filename: luci-app-mqtt-bridge_0.4.0-r4_all.ipk -Size: 22689 +Size: 22687 Package: luci-app-ndpid Version: 1.1.2-r2 @@ -520,7 +520,7 @@ Architecture: all Installed-Size: 122880 Description: Modern dashboard for nDPId deep packet inspection on OpenWrt Filename: luci-app-ndpid_1.1.2-r2_all.ipk -Size: 21704 +Size: 21701 Package: luci-app-netdata-dashboard Version: 0.5.0-r2 @@ -532,7 +532,7 @@ Architecture: all Installed-Size: 112640 Description: Real-time system monitoring dashboard with Netdata integration for OpenWrt Filename: luci-app-netdata-dashboard_0.5.0-r2_all.ipk -Size: 20558 +Size: 20559 Package: luci-app-network-anomaly Version: 1.0.0-r1 @@ -544,7 +544,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI Network Anomaly Detection Dashboard Filename: luci-app-network-anomaly_1.0.0-r1_all.ipk -Size: 7641 +Size: 7643 Package: luci-app-network-modes Version: 0.5.0-r3 @@ -568,7 +568,7 @@ Architecture: all Installed-Size: 81920 Description: Unified network services dashboard with DNS/hosts sync, CDN cache control, and WPAD auto-proxy configuration Filename: luci-app-network-tweaks_1.0.0-r7_all.ipk -Size: 15949 +Size: 15947 Package: luci-app-nextcloud Version: 1.0.0-r1 @@ -580,7 +580,7 @@ Architecture: all Installed-Size: 51200 Description: LuCI support for Nextcloud LXC Filename: luci-app-nextcloud_1.0.0-r1_all.ipk -Size: 10347 +Size: 10348 Package: luci-app-ollama Version: 0.1.0-r1 @@ -592,7 +592,7 @@ Architecture: all Installed-Size: 71680 Description: Modern dashboard for Ollama LLM management on OpenWrt Filename: luci-app-ollama_0.1.0-r1_all.ipk -Size: 14338 +Size: 14337 Package: luci-app-peertube Version: 0 @@ -604,7 +604,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI PeerTube Video Platform Filename: luci-app-peertube_0_all.ipk -Size: 5760 +Size: 5757 Package: luci-app-picobrew Version: 1.0.0-r1 @@ -616,7 +616,7 @@ Architecture: all Installed-Size: 51200 Description: Modern dashboard for PicoBrew Server management on OpenWrt Filename: luci-app-picobrew_1.0.0-r1_all.ipk -Size: 9534 +Size: 9529 Package: luci-app-secubox Version: 0.7.1-r4 @@ -628,7 +628,7 @@ Architecture: all Installed-Size: 440320 Description: Central control hub for all SecuBox modules. Provides unified dashboard, module status, system health monitoring, and quick actions. Filename: luci-app-secubox_0.7.1-r4_all.ipk -Size: 82095 +Size: 82096 Package: luci-app-secubox-admin Version: 1.0.0-r19 @@ -639,7 +639,7 @@ Architecture: all Installed-Size: 337920 Description: Unified admin control center for SecuBox appstore plugins with system monitoring Filename: luci-app-secubox-admin_1.0.0-r19_all.ipk -Size: 58041 +Size: 58040 Package: luci-app-secubox-crowdsec Version: 1.0.0-r3 @@ -651,7 +651,7 @@ Architecture: all Installed-Size: 81920 Description: LuCI SecuBox CrowdSec Dashboard Filename: luci-app-secubox-crowdsec_1.0.0-r3_all.ipk -Size: 13918 +Size: 13916 Package: luci-app-secubox-mirror Version: 0.1.0-r1 @@ -675,7 +675,7 @@ Architecture: all Installed-Size: 81920 Description: Real-time DSA switch port statistics, error monitoring, and network health diagnostics Filename: luci-app-secubox-netdiag_1.0.0-r1_all.ipk -Size: 15347 +Size: 15344 Package: luci-app-secubox-netifyd Version: 1.2.1-r1 @@ -699,7 +699,7 @@ Architecture: all Installed-Size: 245760 Description: LuCI SecuBox P2P Hub Filename: luci-app-secubox-p2p_0.1.0-r1_all.ipk -Size: 46832 +Size: 46831 Package: luci-app-secubox-portal Version: 0.7.0-r3 @@ -711,7 +711,7 @@ Architecture: all Installed-Size: 194560 Description: Unified entry point for all SecuBox applications with tabbed navigation Filename: luci-app-secubox-portal_0.7.0-r3_all.ipk -Size: 41686 +Size: 41681 Package: luci-app-secubox-security-threats Version: 1.0.0-r4 @@ -723,7 +723,18 @@ Architecture: all Installed-Size: 61440 Description: Unified dashboard integrating netifyd DPI threats with CrowdSec intelligence for real-time threat monitoring and automated blocking Filename: luci-app-secubox-security-threats_1.0.0-r4_all.ipk -Size: 10659 +Size: 10657 + +Package: luci-app-secubox-users +Version: 1.0.0-r1 +Depends: secubox-core-users +Section: luci +Maintainer: OpenWrt LuCI community +Architecture: all +Installed-Size: 30720 +Description: LuCI SecuBox User Management +Filename: luci-app-secubox-users_1.0.0-r1_all.ipk +Size: 5141 Package: luci-app-service-registry Version: 1.0.0-r1 @@ -735,7 +746,7 @@ Architecture: all Installed-Size: 194560 Description: Unified service aggregation with HAProxy vhosts, Tor hidden services, and QR-coded landing page Filename: luci-app-service-registry_1.0.0-r1_all.ipk -Size: 39952 +Size: 39951 Package: luci-app-simplex Version: 1.0.0-r1 @@ -747,7 +758,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI SimpleX Chat Server Configuration Filename: luci-app-simplex_1.0.0-r1_all.ipk -Size: 7036 +Size: 7042 Package: luci-app-streamlit Version: 1.0.0-r11 @@ -759,7 +770,7 @@ Architecture: all Installed-Size: 112640 Description: Multi-instance Streamlit management with Gitea integration Filename: luci-app-streamlit_1.0.0-r11_all.ipk -Size: 20568 +Size: 20567 Package: luci-app-system-hub Version: 0.5.1-r4 @@ -771,7 +782,7 @@ Architecture: all Installed-Size: 327680 Description: Central system control with monitoring, services, logs, and backup Filename: luci-app-system-hub_0.5.1-r4_all.ipk -Size: 62082 +Size: 62081 Package: luci-app-threat-analyst Version: 1.0.0-r1 @@ -783,7 +794,7 @@ Architecture: all Installed-Size: 51200 Description: LuCI Threat Analyst Dashboard Filename: luci-app-threat-analyst_1.0.0-r1_all.ipk -Size: 10144 +Size: 10145 Package: luci-app-tor Version: 1.0.0-r1 @@ -795,7 +806,7 @@ Architecture: all Installed-Size: 92160 Description: Modern dashboard for Tor anonymization on OpenWrt Filename: luci-app-tor_1.0.0-r1_all.ipk -Size: 17822 +Size: 17816 Package: luci-app-tor-shield Version: 1.0.0-r10 @@ -807,7 +818,7 @@ Architecture: all Installed-Size: 122880 Description: Modern dashboard for Tor anonymization on OpenWrt Filename: luci-app-tor-shield_1.0.0-r10_all.ipk -Size: 22766 +Size: 22764 Package: luci-app-traffic-shaper Version: 0.4.0-r2 @@ -819,7 +830,7 @@ Architecture: all Installed-Size: 81920 Description: Advanced traffic shaping with TC/CAKE for precise bandwidth control Filename: luci-app-traffic-shaper_0.4.0-r2_all.ipk -Size: 14591 +Size: 14587 Package: luci-app-vhost-manager Version: 0.5.0-r5 @@ -831,7 +842,7 @@ Architecture: all Installed-Size: 153600 Description: Nginx reverse proxy manager with Let's Encrypt SSL certificates, authentication, and WebSocket support Filename: luci-app-vhost-manager_0.5.0-r5_all.ipk -Size: 26284 +Size: 26283 Package: luci-app-voip Version: 1.0.0-r1 @@ -843,7 +854,7 @@ Architecture: all Installed-Size: 81920 Description: LuCI VoIP PBX Management Filename: luci-app-voip_1.0.0-r1_all.ipk -Size: 11046 +Size: 11043 Package: luci-app-vortex-dns Version: 1.0.0-r1 @@ -855,7 +866,7 @@ Architecture: all Installed-Size: 40960 Description: LuCI Vortex DNS Dashboard Filename: luci-app-vortex-dns_1.0.0-r1_all.ipk -Size: 6078 +Size: 6079 Package: luci-app-vortex-firewall Version: 1.0.0-r1 @@ -867,7 +878,7 @@ Architecture: all Installed-Size: 30720 Description: LuCI Vortex DNS Firewall Dashboard Filename: luci-app-vortex-firewall_1.0.0-r1_all.ipk -Size: 5451 +Size: 5450 Package: luci-app-wazuh Version: 1.0.0-r1 @@ -878,7 +889,7 @@ Architecture: all Installed-Size: 71680 Description: Unified security monitoring dashboard for Wazuh SIEM/XDR integration Filename: luci-app-wazuh_1.0.0-r1_all.ipk -Size: 11072 +Size: 11073 Package: luci-app-wireguard-dashboard Version: 0.7.0-r5 @@ -890,7 +901,7 @@ Architecture: all Installed-Size: 215040 Description: Modern dashboard for WireGuard VPN monitoring on OpenWrt Filename: luci-app-wireguard-dashboard_0.7.0-r5_all.ipk -Size: 42291 +Size: 42286 Package: luci-app-zigbee2mqtt Version: 1.0.0-r2 @@ -902,7 +913,7 @@ Architecture: all Installed-Size: 40960 Description: Graphical interface for managing the Zigbee2MQTT LXC application. Filename: luci-app-zigbee2mqtt_1.0.0-r2_all.ipk -Size: 6591 +Size: 6592 Package: luci-theme-secubox Version: 0.4.7-r1 @@ -914,7 +925,7 @@ Architecture: all Installed-Size: 450560 Description: Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards. Filename: luci-theme-secubox_0.4.7-r1_all.ipk -Size: 110239 +Size: 110238 Package: secubox-app Version: 1.0.0-r2 @@ -925,7 +936,7 @@ Installed-Size: 92160 Description: Command line helper for SecuBox App Store manifests. Installs /usr/sbin/secubox-app and ships the default manifests under /usr/share/secubox/plugins/. Filename: secubox-app_1.0.0-r2_all.ipk -Size: 11182 +Size: 11183 Package: secubox-app-adguardhome Version: 1.0.0-r2 @@ -939,7 +950,7 @@ Description: Installer, configuration, and service manager for running AdGuard inside Docker on SecuBox-powered OpenWrt systems. Network-wide ad blocker with DNS-over-HTTPS/TLS support and detailed analytics. Filename: secubox-app-adguardhome_1.0.0-r2_all.ipk -Size: 2883 +Size: 2880 Package: secubox-app-auth-logger Version: 1.2.2-r1 @@ -957,7 +968,7 @@ Description: Logs authentication failures from LuCI/rpcd and Dropbear SSH - JavaScript hook to intercept login failures - CrowdSec parser and bruteforce scenario Filename: secubox-app-auth-logger_1.2.2-r1_all.ipk -Size: 9377 +Size: 9374 Package: secubox-app-crowdsec-custom Version: 1.1.0-r1 @@ -1009,7 +1020,7 @@ Description: SecuBox CrowdSec Firewall Bouncer for OpenWrt. - Automatic restart on firewall reload - procd service management Filename: secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk -Size: 5049325 +Size: 5049324 Package: secubox-app-cyberfeed Version: 0.2.1-r1 @@ -1023,7 +1034,7 @@ Description: Cyberpunk-themed RSS feed aggregator for OpenWrt/SecuBox. Features emoji injection, neon styling, and RSS-Bridge support for social media feeds (Facebook, Twitter, Mastodon). Filename: secubox-app-cyberfeed_0.2.1-r1_all.ipk -Size: 12453 +Size: 12450 Package: secubox-app-device-intel Version: 1.0.0-r1 @@ -1037,7 +1048,7 @@ Description: Unified device inventory aggregating mac-guardian, client-guardian P2P mesh, and exposure scanner data. Includes heuristic classification and pluggable emulator modules for MQTT, Zigbee, and USB devices. Filename: secubox-app-device-intel_1.0.0-r1_all.ipk -Size: 13108 +Size: 13104 Package: secubox-app-dns-provider Version: 1.0.0-r1 @@ -1051,7 +1062,7 @@ Description: Programmatic DNS record management via provider APIs (OVH, Gandi Cloudflare). Provides the dnsctl CLI for record CRUD, zone sync DNS propagation verification, and ACME DNS-01 challenge support. Filename: secubox-app-dns-provider_1.0.0-r1_all.ipk -Size: 8261 +Size: 8257 Package: secubox-app-domoticz Version: 1.0.0-r4 @@ -1064,7 +1075,7 @@ Installed-Size: 30720 Description: Installer, configuration, and service manager for running Domoticz inside an LXC Alpine container on SecuBox-powered OpenWrt systems. Filename: secubox-app-domoticz_1.0.0-r4_all.ipk -Size: 7503 +Size: 7507 Package: secubox-app-exposure Version: 1.0.0-r1 @@ -1079,7 +1090,7 @@ Description: Unified service exposure manager for SecuBox. - Dynamic Tor hidden service management - HAProxy SSL reverse proxy configuration Filename: secubox-app-exposure_1.0.0-r1_all.ipk -Size: 9147 +Size: 9145 Package: secubox-app-gitea Version: 1.0.0-r5 @@ -1102,7 +1113,7 @@ Description: Gitea Git Platform - Self-hosted lightweight Git service Runs in LXC container with Alpine Linux. Configure in /etc/config/gitea. Filename: secubox-app-gitea_1.0.0-r5_all.ipk -Size: 9439 +Size: 9441 Package: secubox-app-gk2hub Version: 0.1.0-r1 @@ -1116,7 +1127,7 @@ Description: Dynamic landing page generator for GK2 SecuBox services. Aggregates Streamlit apps, MetaBlogizer sites, and infrastructure services into a single service directory page. Filename: secubox-app-gk2hub_0.1.0-r1_all.ipk -Size: 4064 +Size: 4059 Package: secubox-app-glances Version: 1.0.0-r1 @@ -1139,7 +1150,7 @@ Description: Glances - Cross-platform system monitoring tool for SecuBox. Runs in LXC container for isolation and security. Configure in /etc/config/glances. Filename: secubox-app-glances_1.0.0-r1_all.ipk -Size: 6142 +Size: 6137 Package: secubox-app-guacamole Version: 1.0.0-r1 @@ -1153,7 +1164,7 @@ Description: Apache Guacamole clientless remote desktop gateway. Runs in an LXC Debian container with guacd and Tomcat. Supports SSH, VNC, and RDP connections via web browser. Filename: secubox-app-guacamole_1.0.0-r1_all.ipk -Size: 6948 +Size: 6945 Package: secubox-app-haproxy Version: 1.0.0-r24 @@ -1173,7 +1184,7 @@ Description: HAProxy load balancer and reverse proxy running in an LXC containe - Stats dashboard - Rate limiting and ACLs Filename: secubox-app-haproxy_1.0.0-r24_all.ipk -Size: 22012 +Size: 22008 Package: secubox-app-hexojs Version: 1.0.0-r8 @@ -1182,7 +1193,7 @@ License: MIT Section: utils Maintainer: CyberMind Studio Architecture: all -Installed-Size: 501760 +Installed-Size: 522240 Description: Hexo CMS - Self-hosted static blog generator for OpenWrt Features: @@ -1197,7 +1208,7 @@ Description: Hexo CMS - Self-hosted static blog generator for OpenWrt Runs in LXC container with Alpine Linux. Configure in /etc/config/hexojs. Filename: secubox-app-hexojs_1.0.0-r8_all.ipk -Size: 94937 +Size: 100060 Package: secubox-app-jabber Version: 1.0.0-r1 @@ -1211,7 +1222,7 @@ Description: Jabber/XMPP instant messaging server based on Prosody. Runs in an LXC Debian container with full XMPP support. Features multi-user chat (MUC), file uploads, and S2S federation. Filename: secubox-app-jabber_1.0.0-r1_all.ipk -Size: 13277 +Size: 13276 Package: secubox-app-jellyfin Version: 3.0.0-r1 @@ -1224,7 +1235,7 @@ Installed-Size: 20480 Description: Jellyfin media server running in LXC container. Free media server for streaming movies, TV shows, music, and photos. Filename: secubox-app-jellyfin_3.0.0-r1_all.ipk -Size: 4748 +Size: 4755 Package: secubox-app-jitsi Version: 1.0.0-r1 @@ -1249,7 +1260,7 @@ Description: Jitsi Meet - Secure, fully featured video conferencing for SecuBox Integrates with HAProxy for SSL termination. Configure in /etc/config/jitsi. Filename: secubox-app-jitsi_1.0.0-r1_all.ipk -Size: 8927 +Size: 8924 Package: secubox-app-localai Version: 3.9.0-r1 @@ -1271,7 +1282,7 @@ Description: LocalAI native binary package for OpenWrt. API: http://:8081/v1 Filename: secubox-app-localai_3.9.0-r1_all.ipk -Size: 5848 +Size: 5841 Package: secubox-app-localai-wb Version: 2.25.0-r1 @@ -1315,7 +1326,7 @@ Description: Lyrion Media Server (formerly Logitech Media Server / Squeezebox S Auto-detects available runtime, preferring LXC for lower resource usage. Configure runtime in /etc/config/lyrion. Filename: secubox-app-lyrion_2.0.2-r1_all.ipk -Size: 8130 +Size: 8124 Package: secubox-app-mac-guardian Version: 0.5.0-r1 @@ -1330,7 +1341,7 @@ Description: WiFi MAC address security monitor for SecuBox. and spoofing. Integrates with CrowdSec and provides real-time hostapd hotplug detection. Filename: secubox-app-mac-guardian_0.5.0-r1_all.ipk -Size: 12098 +Size: 12097 Package: secubox-app-magicmirror2 Version: 0.4.0-r8 @@ -1377,7 +1388,7 @@ Description: Complete email server solution using docker-mailserver for SecuBox Commands: mailinaboxctl --help Filename: secubox-app-mailinabox_2.0.0-r1_all.ipk -Size: 7569 +Size: 7574 Package: secubox-app-mailserver Version: 2.0.0-r1 @@ -1390,7 +1401,7 @@ Installed-Size: 20480 Description: Postfix + Dovecot mail server running in LXC container. Supports IMAP/SMTP with SSL/TLS. Filename: secubox-app-mailserver_2.0.0-r1_all.ipk -Size: 5697 +Size: 5693 Package: secubox-app-matrix Version: 1.0.0-r1 @@ -1425,7 +1436,7 @@ Description: Metabolizer Blog Pipeline - Integrated CMS with Git-based workflow Pipeline: Edit in Streamlit -> Push to Gitea -> Build with Hexo -> Publish Filename: secubox-app-metabolizer_1.0.0-r3_all.ipk -Size: 13980 +Size: 13975 Package: secubox-app-mitmproxy Version: 0.5.0-r19 @@ -1452,7 +1463,7 @@ Description: mitmproxy - Interactive HTTPS proxy for SecuBox-powered OpenWrt sy Runs in LXC container for isolation and security. Configure in /etc/config/mitmproxy. Filename: secubox-app-mitmproxy_0.5.0-r19_all.ipk -Size: 22958 +Size: 22957 Package: secubox-app-mmpm Version: 0.2.0-r5 @@ -1473,7 +1484,7 @@ Description: MMPM (MagicMirror Package Manager) for SecuBox. Runs inside the MagicMirror2 LXC container. Filename: secubox-app-mmpm_0.2.0-r5_all.ipk -Size: 3978 +Size: 3979 Package: secubox-app-nextcloud Version: 1.0.0-r2 @@ -1509,7 +1520,7 @@ Description: Ollama - Simple local LLM runtime for SecuBox-powered OpenWrt syst Runs in Docker/Podman container. Configure in /etc/config/ollama. Filename: secubox-app-ollama_0.1.0-r1_all.ipk -Size: 5735 +Size: 5736 Package: secubox-app-picobrew Version: 1.0.0-r7 @@ -1544,7 +1555,7 @@ Installed-Size: 20480 Description: Self-hosted RustDesk relay server for remote desktop access. Downloads and manages hbbs (ID server) and hbbr (relay server) binaries. Filename: secubox-app-rustdesk_1.0.0-r1_all.ipk -Size: 4469 +Size: 4467 Package: secubox-app-simplex Version: 1.0.0-r1 @@ -1568,7 +1579,7 @@ Description: SimpleX Chat self-hosted messaging infrastructure for SecuBox. Privacy-first messaging relay that you control. Configure in /etc/config/simplex. Filename: secubox-app-simplex_1.0.0-r1_all.ipk -Size: 9369 +Size: 9367 Package: secubox-app-smbfs Version: 1.0.0-r1 @@ -1582,7 +1593,7 @@ Description: SMB/CIFS remote directory mount manager for SecuBox. Manages share network mounts for media servers (Jellyfin, Lyrion), backups, and general-purpose remote storage over SMB/CIFS protocol. Filename: secubox-app-smbfs_1.0.0-r1_all.ipk -Size: 5269 +Size: 5268 Package: secubox-app-streamlit Version: 1.0.0-r5 @@ -1609,7 +1620,7 @@ Description: Streamlit App Platform - Self-hosted Python data app platform Configure in /etc/config/streamlit. Filename: secubox-app-streamlit_1.0.0-r5_all.ipk -Size: 16517 +Size: 16519 Package: secubox-app-tor Version: 1.0.0-r1 @@ -1632,7 +1643,7 @@ Description: SecuBox Tor Shield - One-click Tor anonymization for OpenWrt Configure in /etc/config/tor-shield. Filename: secubox-app-tor_1.0.0-r1_all.ipk -Size: 7372 +Size: 7370 Package: secubox-app-voip Version: 1.0.0-r1 @@ -1646,7 +1657,7 @@ Description: VoIP PBX solution with Asterisk in LXC container. Features OVH SIP trunk integration, WebRTC support and Jabber/XMPP relay for SMS and voicemail notifications. Filename: secubox-app-voip_1.0.0-r1_all.ipk -Size: 11955 +Size: 11954 Package: secubox-app-webapp Version: 1.5.0-r7 @@ -1664,7 +1675,7 @@ Description: SecuBox Control Center Dashboard - A web-based dashboard for monit - Service management - Network interface control Filename: secubox-app-webapp_1.5.0-r7_all.ipk -Size: 39176 +Size: 39177 Package: secubox-app-zigbee2mqtt Version: 1.0.0-r3 @@ -1677,7 +1688,7 @@ Installed-Size: 20480 Description: Installer, configuration, and service manager for running Zigbee2MQTT inside an Alpine LXC container on SecuBox-powered OpenWrt systems. Filename: secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk -Size: 5533 +Size: 5539 Package: secubox-config-advisor Version: 0.1.0-r1 @@ -1696,7 +1707,7 @@ Description: AI-powered configuration security advisor for SecuBox. - LocalAI integration for intelligent analysis - Automated remediation suggestions Filename: secubox-config-advisor_0.1.0-r1_all.ipk -Size: 14850 +Size: 14849 Package: secubox-content-pkg Version: 1.0.0-r1 @@ -1709,7 +1720,7 @@ Installed-Size: 20480 Description: Package Metablogizer sites and Streamlit apps as IPKs for P2P distribution. Auto-publishes content to the mesh feed for peer auto-sync. Filename: secubox-content-pkg_1.0.0-r1_all.ipk -Size: 3914 +Size: 3904 Package: secubox-cookie-tracker Version: 1.0.0-r1 @@ -1732,7 +1743,7 @@ Description: Cookie Tracker for SecuBox InterceptoR. Works with secubox-app-mitmproxy for transparent interception. Filename: secubox-cookie-tracker_1.0.0-r1_all.ipk -Size: 10645 +Size: 10641 Package: secubox-core Version: 0.10.0-r16 @@ -1752,7 +1763,7 @@ Description: SecuBox Core Framework provides the foundational infrastructure fo - Unified CLI interface - ubus RPC backend Filename: secubox-core_0.10.0-r16_all.ipk -Size: 123047 +Size: 123045 Package: secubox-cve-triage Version: 1.0.0-r1 @@ -1772,7 +1783,7 @@ Description: AI-powered CVE analysis and vulnerability management agent for Sec - Approval workflow for patch recommendations - LXC and Docker package monitoring Filename: secubox-cve-triage_1.0.0-r1_all.ipk -Size: 11826 +Size: 11827 Package: secubox-dns-guard Version: 1.0.0-r1 @@ -1791,7 +1802,7 @@ Description: SecuBox DNS Guard provides AI-powered DNS anomaly detection using - Unusual TLD pattern detection - Automatic blocklist generation with approval workflow Filename: secubox-dns-guard_1.0.0-r1_all.ipk -Size: 12488 +Size: 12485 Package: secubox-identity Version: 0.1.0-r1 @@ -1810,7 +1821,7 @@ Description: Decentralized identity management for SecuBox mesh nodes. - Peer identity verification - Trust scoring integration Filename: secubox-identity_0.1.0-r1_all.ipk -Size: 8089 +Size: 8088 Package: secubox-iot-guard Version: 1.0.0-r1 @@ -1826,7 +1837,7 @@ Description: IoT device isolation, classification, and security monitoring. risk scoring. Orchestrates Client Guardian, MAC Guardian Vortex Firewall, and Bandwidth Manager for IoT protection. Filename: secubox-iot-guard_1.0.0-r1_all.ipk -Size: 13374 +Size: 13365 Package: secubox-localrecall Version: 1.0.0-r1 @@ -1841,7 +1852,7 @@ Description: Persistent memory system for SecuBox AI agents. for context across sessions. LocalAI integration for semantic search and AI-powered summarization. Filename: secubox-localrecall_1.0.0-r1_all.ipk -Size: 7802 +Size: 7796 Package: secubox-master-link Version: 1.0.0-r1 @@ -1863,7 +1874,7 @@ Description: Secure mesh onboarding for SecuBox nodes via master/peer link. Configure in /etc/config/master-link. Filename: secubox-master-link_1.0.0-r1_all.ipk -Size: 15038 +Size: 15034 Package: secubox-mcp-server Version: 1.0.0-r1 @@ -1891,7 +1902,7 @@ Description: Model Context Protocol (MCP) server for SecuBox. - ai.explain_ban (Explain CrowdSec decisions) - ai.security_posture (Security assessment) Filename: secubox-mcp-server_1.0.0-r1_all.ipk -Size: 11433 +Size: 11427 Package: secubox-mirrornet Version: 0.1.0-r1 @@ -1909,7 +1920,7 @@ Description: MirrorNet core mesh orchestration for SecuBox. - Mesh health monitoring and anomaly detection - DID-based identity (did:plc compatible) Filename: secubox-mirrornet_0.1.0-r1_all.ipk -Size: 15306 +Size: 15303 Package: secubox-network-anomaly Version: 1.0.0-r1 @@ -1924,7 +1935,7 @@ Description: AI-powered network anomaly detection for SecuBox. DNS anomalies, and protocol anomalies using statistical analysis and optional LocalAI integration. Filename: secubox-network-anomaly_1.0.0-r1_all.ipk -Size: 6170 +Size: 6163 Package: secubox-p2p Version: 0.6.0-r3 @@ -1943,7 +1954,7 @@ Description: SecuBox P2P Hub backend providing peer discovery, mesh networking and MirrorBox NetMesh Catalog for cross-chain distributed service registry with HAProxy vhost discovery and multi-endpoint access URLs. Filename: secubox-p2p_0.6.0-r3_all.ipk -Size: 47872 +Size: 47860 Package: secubox-p2p-intel Version: 0.1.0-r1 @@ -1962,7 +1973,7 @@ Description: Decentralized threat intelligence sharing for SecuBox mesh. - CrowdSec and mitmproxy integration - Automatic firewall rule application Filename: secubox-p2p-intel_0.1.0-r1_all.ipk -Size: 9798 +Size: 9799 Package: secubox-threat-analyst Version: 1.0.0-r1 @@ -1981,7 +1992,7 @@ Description: Autonomous threat analysis agent for SecuBox. Part of SecuBox AI Gateway (Couche 2). Filename: secubox-threat-analyst_1.0.0-r1_all.ipk -Size: 9867 +Size: 9864 Package: secubox-vortex-dns Version: 1.0.0-r1 @@ -2000,7 +2011,7 @@ Description: Meshed multi-dynamic subdomain delegation system for SecuBox. - Gossip-based exposure config sync - Submastering for nested hierarchies Filename: secubox-vortex-dns_1.0.0-r1_all.ipk -Size: 5443 +Size: 5439 Package: secubox-vortex-firewall Version: 1.0.0-r1 @@ -2015,5 +2026,5 @@ Description: DNS-level threat blocking with x47 impact multiplier. any connection is established. Integrates threat feeds from abuse.ch, OpenPhish, and local DNS Guard detections. Filename: secubox-vortex-firewall_1.0.0-r1_all.ipk -Size: 8898 +Size: 8892 diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages.gz b/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages.gz index 1f240ed3..218d509b 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages.gz and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages.gz differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/apps-local.json b/package/secubox/secubox-app-bonus/root/www/secubox-feed/apps-local.json index c97c64c4..10b243bb 100644 --- a/package/secubox/secubox-app-bonus/root/www/secubox-feed/apps-local.json +++ b/package/secubox/secubox-app-bonus/root/www/secubox-feed/apps-local.json @@ -1,12 +1,12 @@ { "feed_url": "/secubox-feed", - "generated": "2026-02-19T18:57:57+01:00", + "generated": "2026-02-20T08:56:23+01:00", "packages": [ { "name": "luci-app-ai-insights", "version": "1.0.0-r1", "filename": "luci-app-ai-insights_1.0.0-r1_all.ipk", - "size": 11638, + "size": 11639, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -18,7 +18,7 @@ "name": "luci-app-auth-guardian", "version": "0.4.0-r3", "filename": "luci-app-auth-guardian_0.4.0-r3_all.ipk", - "size": 12400, + "size": 12391, "category": "security", "icon": "key", "description": "Authentication management", @@ -30,7 +30,7 @@ "name": "luci-app-backup", "version": "1.0.0-r1", "filename": "luci-app-backup_1.0.0-r1_all.ipk", - "size": 4534, + "size": 4537, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -54,7 +54,7 @@ "name": "luci-app-cdn-cache", "version": "0.5.0-r3", "filename": "luci-app-cdn-cache_0.5.0-r3_all.ipk", - "size": 24255, + "size": 24257, "category": "network", "icon": "globe", "description": "CDN caching", @@ -66,7 +66,7 @@ "name": "luci-app-client-guardian", "version": "0.4.0-r7", "filename": "luci-app-client-guardian_0.4.0-r7_all.ipk", - "size": 52684, + "size": 52687, "category": "network", "icon": "users", "description": "Client management and monitoring", @@ -78,7 +78,7 @@ "name": "luci-app-cloner", "version": "1.0.0-r1", "filename": "luci-app-cloner_1.0.0-r1_all.ipk", - "size": 19433, + "size": 19435, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -102,7 +102,7 @@ "name": "luci-app-cookie-tracker", "version": "1.0.0-r1", "filename": "luci-app-cookie-tracker_1.0.0-r1_all.ipk", - "size": 5653, + "size": 5660, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -114,7 +114,7 @@ "name": "luci-app-crowdsec-dashboard", "version": "0.7.0-r32", "filename": "luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk", - "size": 34938, + "size": 34940, "category": "security", "icon": "shield", "description": "CrowdSec security monitoring", @@ -126,7 +126,7 @@ "name": "luci-app-cve-triage", "version": "1.0.0-r1", "filename": "luci-app-cve-triage_1.0.0-r1_all.ipk", - "size": 5948, + "size": 5943, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -150,7 +150,7 @@ "name": "luci-app-device-intel", "version": "1.0.0-r1", "filename": "luci-app-device-intel_1.0.0-r1_all.ipk", - "size": 12047, + "size": 12049, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -162,7 +162,7 @@ "name": "luci-app-dnsguard", "version": "1.1.0-r1", "filename": "luci-app-dnsguard_1.1.0-r1_all.ipk", - "size": 12448, + "size": 12449, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -174,7 +174,7 @@ "name": "luci-app-dns-provider", "version": "1.0.0-r1", "filename": "luci-app-dns-provider_1.0.0-r1_all.ipk", - "size": 7168, + "size": 7174, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -186,7 +186,7 @@ "name": "luci-app-domoticz", "version": "1.0.0-r1", "filename": "luci-app-domoticz_1.0.0-r1_all.ipk", - "size": 7123, + "size": 7118, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -198,7 +198,7 @@ "name": "luci-app-exposure", "version": "1.0.0-r3", "filename": "luci-app-exposure_1.0.0-r3_all.ipk", - "size": 11696, + "size": 11698, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -210,7 +210,7 @@ "name": "luci-app-gitea", "version": "1.0.0-r2", "filename": "luci-app-gitea_1.0.0-r2_all.ipk", - "size": 16623, + "size": 16621, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -222,7 +222,7 @@ "name": "luci-app-glances", "version": "1.0.0-r2", "filename": "luci-app-glances_1.0.0-r2_all.ipk", - "size": 7022, + "size": 7017, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -234,7 +234,7 @@ "name": "luci-app-gotosocial", "version": "0.1.0-r1", "filename": "luci-app-gotosocial_0.1.0-r1_all.ipk", - "size": 8207, + "size": 8210, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -258,7 +258,7 @@ "name": "luci-app-hexojs", "version": "1.0.0-r3", "filename": "luci-app-hexojs_1.0.0-r3_all.ipk", - "size": 30452, + "size": 30450, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -270,7 +270,7 @@ "name": "luci-app-iot-guard", "version": "1.0.0-r1", "filename": "luci-app-iot-guard_1.0.0-r1_all.ipk", - "size": 10535, + "size": 10538, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -282,7 +282,7 @@ "name": "luci-app-jabber", "version": "0", "filename": "luci-app-jabber_0_all.ipk", - "size": 9304, + "size": 9308, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -294,7 +294,7 @@ "name": "luci-app-jellyfin", "version": "1.0.0-r1", "filename": "luci-app-jellyfin_1.0.0-r1_all.ipk", - "size": 10482, + "size": 10485, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -306,7 +306,7 @@ "name": "luci-app-jitsi", "version": "1.0.0-r1", "filename": "luci-app-jitsi_1.0.0-r1_all.ipk", - "size": 5175, + "size": 5173, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -318,7 +318,7 @@ "name": "luci-app-ksm-manager", "version": "0.4.0-r2", "filename": "luci-app-ksm-manager_0.4.0-r2_all.ipk", - "size": 18779, + "size": 18777, "category": "system", "icon": "cpu", "description": "Kernel memory management", @@ -342,7 +342,7 @@ "name": "luci-app-localrecall", "version": "1.0.0-r1", "filename": "luci-app-localrecall_1.0.0-r1_all.ipk", - "size": 8418, + "size": 8420, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -354,7 +354,7 @@ "name": "luci-app-lyrion", "version": "1.0.0-r1", "filename": "luci-app-lyrion_1.0.0-r1_all.ipk", - "size": 6839, + "size": 6840, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -366,7 +366,7 @@ "name": "luci-app-mac-guardian", "version": "0.5.0-r1", "filename": "luci-app-mac-guardian_0.5.0-r1_all.ipk", - "size": 6664, + "size": 6660, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -378,7 +378,7 @@ "name": "luci-app-magicmirror2", "version": "0.4.0-r6", "filename": "luci-app-magicmirror2_0.4.0-r6_all.ipk", - "size": 12361, + "size": 12358, "category": "iot", "icon": "monitor", "description": "Smart mirror display", @@ -390,7 +390,7 @@ "name": "luci-app-mailinabox", "version": "1.0.0-r1", "filename": "luci-app-mailinabox_1.0.0-r1_all.ipk", - "size": 5484, + "size": 5487, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -402,7 +402,7 @@ "name": "luci-app-mailserver", "version": "1.0.0-r1", "filename": "luci-app-mailserver_1.0.0-r1_all.ipk", - "size": 6511, + "size": 6509, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -414,7 +414,7 @@ "name": "luci-app-master-link", "version": "1.0.0-r1", "filename": "luci-app-master-link_1.0.0-r1_all.ipk", - "size": 6304, + "size": 6309, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -438,7 +438,7 @@ "name": "luci-app-media-flow", "version": "0.6.4-r1", "filename": "luci-app-media-flow_0.6.4-r1_all.ipk", - "size": 25381, + "size": 25379, "category": "media", "icon": "film", "description": "Media streaming", @@ -450,7 +450,7 @@ "name": "luci-app-metablogizer", "version": "1.1.0-r1", "filename": "luci-app-metablogizer_1.1.0-r1_all.ipk", - "size": 26207, + "size": 26203, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -462,7 +462,7 @@ "name": "luci-app-metabolizer", "version": "1.0.0-r2", "filename": "luci-app-metabolizer_1.0.0-r2_all.ipk", - "size": 4821, + "size": 4819, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -474,7 +474,7 @@ "name": "luci-app-mitmproxy", "version": "0.5.0-r2", "filename": "luci-app-mitmproxy_0.5.0-r2_all.ipk", - "size": 13232, + "size": 13234, "category": "security", "icon": "lock", "description": "HTTPS proxy and traffic inspection", @@ -486,7 +486,7 @@ "name": "luci-app-mmpm", "version": "0.2.0-r3", "filename": "luci-app-mmpm_0.2.0-r3_all.ipk", - "size": 7975, + "size": 7971, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -498,7 +498,7 @@ "name": "luci-app-mqtt-bridge", "version": "0.4.0-r4", "filename": "luci-app-mqtt-bridge_0.4.0-r4_all.ipk", - "size": 22689, + "size": 22687, "category": "iot", "icon": "message-square", "description": "MQTT bridge", @@ -510,7 +510,7 @@ "name": "luci-app-ndpid", "version": "1.1.2-r2", "filename": "luci-app-ndpid_1.1.2-r2_all.ipk", - "size": 21704, + "size": 21701, "category": "security", "icon": "eye", "description": "Deep packet inspection", @@ -522,7 +522,7 @@ "name": "luci-app-netdata-dashboard", "version": "0.5.0-r2", "filename": "luci-app-netdata-dashboard_0.5.0-r2_all.ipk", - "size": 20558, + "size": 20559, "category": "monitoring", "icon": "bar-chart-2", "description": "System monitoring dashboard", @@ -534,7 +534,7 @@ "name": "luci-app-network-anomaly", "version": "1.0.0-r1", "filename": "luci-app-network-anomaly_1.0.0-r1_all.ipk", - "size": 7641, + "size": 7643, "category": "network", "icon": "wifi", "description": "Network configuration", @@ -558,7 +558,7 @@ "name": "luci-app-network-tweaks", "version": "1.0.0-r7", "filename": "luci-app-network-tweaks_1.0.0-r7_all.ipk", - "size": 15949, + "size": 15947, "category": "network", "icon": "wifi", "description": "Network configuration", @@ -570,7 +570,7 @@ "name": "luci-app-nextcloud", "version": "1.0.0-r1", "filename": "luci-app-nextcloud_1.0.0-r1_all.ipk", - "size": 10347, + "size": 10348, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -582,7 +582,7 @@ "name": "luci-app-ollama", "version": "0.1.0-r1", "filename": "luci-app-ollama_0.1.0-r1_all.ipk", - "size": 14338, + "size": 14337, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -594,7 +594,7 @@ "name": "luci-app-peertube", "version": "0", "filename": "luci-app-peertube_0_all.ipk", - "size": 5760, + "size": 5757, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -606,7 +606,7 @@ "name": "luci-app-picobrew", "version": "1.0.0-r1", "filename": "luci-app-picobrew_1.0.0-r1_all.ipk", - "size": 9534, + "size": 9529, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -618,7 +618,7 @@ "name": "luci-app-secubox", "version": "0.7.1-r4", "filename": "luci-app-secubox_0.7.1-r4_all.ipk", - "size": 82095, + "size": 82096, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -630,7 +630,7 @@ "name": "luci-app-secubox-admin", "version": "1.0.0-r19", "filename": "luci-app-secubox-admin_1.0.0-r19_all.ipk", - "size": 58041, + "size": 58040, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -642,7 +642,7 @@ "name": "luci-app-secubox-crowdsec", "version": "1.0.0-r3", "filename": "luci-app-secubox-crowdsec_1.0.0-r3_all.ipk", - "size": 13918, + "size": 13916, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -666,7 +666,7 @@ "name": "luci-app-secubox-netdiag", "version": "1.0.0-r1", "filename": "luci-app-secubox-netdiag_1.0.0-r1_all.ipk", - "size": 15347, + "size": 15344, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -690,7 +690,7 @@ "name": "luci-app-secubox-p2p", "version": "0.1.0-r1", "filename": "luci-app-secubox-p2p_0.1.0-r1_all.ipk", - "size": 46832, + "size": 46831, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -702,7 +702,7 @@ "name": "luci-app-secubox-portal", "version": "0.7.0-r3", "filename": "luci-app-secubox-portal_0.7.0-r3_all.ipk", - "size": 41686, + "size": 41681, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -714,7 +714,19 @@ "name": "luci-app-secubox-security-threats", "version": "1.0.0-r4", "filename": "luci-app-secubox-security-threats_1.0.0-r4_all.ipk", - "size": 10659, + "size": 10657, + "category": "system", + "icon": "box", + "description": "SecuBox system component", + "installed": false, + "luci_app": null + } +, + { + "name": "luci-app-secubox-users", + "version": "1.0.0-r1", + "filename": "luci-app-secubox-users_1.0.0-r1_all.ipk", + "size": 5141, "category": "system", "icon": "box", "description": "SecuBox system component", @@ -726,7 +738,7 @@ "name": "luci-app-service-registry", "version": "1.0.0-r1", "filename": "luci-app-service-registry_1.0.0-r1_all.ipk", - "size": 39952, + "size": 39951, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -738,7 +750,7 @@ "name": "luci-app-simplex", "version": "1.0.0-r1", "filename": "luci-app-simplex_1.0.0-r1_all.ipk", - "size": 7036, + "size": 7042, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -750,7 +762,7 @@ "name": "luci-app-streamlit", "version": "1.0.0-r11", "filename": "luci-app-streamlit_1.0.0-r11_all.ipk", - "size": 20568, + "size": 20567, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -762,7 +774,7 @@ "name": "luci-app-system-hub", "version": "0.5.1-r4", "filename": "luci-app-system-hub_0.5.1-r4_all.ipk", - "size": 62082, + "size": 62081, "category": "system", "icon": "settings", "description": "System management", @@ -774,7 +786,7 @@ "name": "luci-app-threat-analyst", "version": "1.0.0-r1", "filename": "luci-app-threat-analyst_1.0.0-r1_all.ipk", - "size": 10144, + "size": 10145, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -786,7 +798,7 @@ "name": "luci-app-tor", "version": "1.0.0-r1", "filename": "luci-app-tor_1.0.0-r1_all.ipk", - "size": 17822, + "size": 17816, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -798,7 +810,7 @@ "name": "luci-app-tor-shield", "version": "1.0.0-r10", "filename": "luci-app-tor-shield_1.0.0-r10_all.ipk", - "size": 22766, + "size": 22764, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -810,7 +822,7 @@ "name": "luci-app-traffic-shaper", "version": "0.4.0-r2", "filename": "luci-app-traffic-shaper_0.4.0-r2_all.ipk", - "size": 14591, + "size": 14587, "category": "network", "icon": "filter", "description": "Traffic shaping and QoS", @@ -822,7 +834,7 @@ "name": "luci-app-vhost-manager", "version": "0.5.0-r5", "filename": "luci-app-vhost-manager_0.5.0-r5_all.ipk", - "size": 26284, + "size": 26283, "category": "network", "icon": "server", "description": "Virtual host management", @@ -834,7 +846,7 @@ "name": "luci-app-voip", "version": "1.0.0-r1", "filename": "luci-app-voip_1.0.0-r1_all.ipk", - "size": 11046, + "size": 11043, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -846,7 +858,7 @@ "name": "luci-app-vortex-dns", "version": "1.0.0-r1", "filename": "luci-app-vortex-dns_1.0.0-r1_all.ipk", - "size": 6078, + "size": 6079, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -858,7 +870,7 @@ "name": "luci-app-vortex-firewall", "version": "1.0.0-r1", "filename": "luci-app-vortex-firewall_1.0.0-r1_all.ipk", - "size": 5451, + "size": 5450, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -870,7 +882,7 @@ "name": "luci-app-wazuh", "version": "1.0.0-r1", "filename": "luci-app-wazuh_1.0.0-r1_all.ipk", - "size": 11072, + "size": 11073, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -882,7 +894,7 @@ "name": "luci-app-wireguard-dashboard", "version": "0.7.0-r5", "filename": "luci-app-wireguard-dashboard_0.7.0-r5_all.ipk", - "size": 42291, + "size": 42286, "category": "vpn", "icon": "shield", "description": "WireGuard VPN dashboard", @@ -894,7 +906,7 @@ "name": "luci-app-zigbee2mqtt", "version": "1.0.0-r2", "filename": "luci-app-zigbee2mqtt_1.0.0-r2_all.ipk", - "size": 6591, + "size": 6592, "category": "iot", "icon": "radio", "description": "Zigbee device management", @@ -906,7 +918,7 @@ "name": "luci-theme-secubox", "version": "0.4.7-r1", "filename": "luci-theme-secubox_0.4.7-r1_all.ipk", - "size": 110239, + "size": 110238, "category": "theme", "icon": "palette", "description": "LuCI theme", @@ -918,7 +930,7 @@ "name": "secubox-app", "version": "1.0.0-r2", "filename": "secubox-app_1.0.0-r2_all.ipk", - "size": 11182, + "size": 11183, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -930,7 +942,7 @@ "name": "secubox-app-adguardhome", "version": "1.0.0-r2", "filename": "secubox-app-adguardhome_1.0.0-r2_all.ipk", - "size": 2883, + "size": 2880, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -942,7 +954,7 @@ "name": "secubox-app-auth-logger", "version": "1.2.2-r1", "filename": "secubox-app-auth-logger_1.2.2-r1_all.ipk", - "size": 9377, + "size": 9374, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -966,7 +978,7 @@ "name": "secubox-app-cs-firewall-bouncer", "version": "0.0.31-r4_aarch64", "filename": "secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk", - "size": 5049325, + "size": 5049324, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -978,7 +990,7 @@ "name": "secubox-app-cyberfeed", "version": "0.2.1-r1", "filename": "secubox-app-cyberfeed_0.2.1-r1_all.ipk", - "size": 12453, + "size": 12450, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -990,7 +1002,7 @@ "name": "secubox-app-device-intel", "version": "1.0.0-r1", "filename": "secubox-app-device-intel_1.0.0-r1_all.ipk", - "size": 13108, + "size": 13104, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1002,7 +1014,7 @@ "name": "secubox-app-dns-provider", "version": "1.0.0-r1", "filename": "secubox-app-dns-provider_1.0.0-r1_all.ipk", - "size": 8261, + "size": 8257, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1014,7 +1026,7 @@ "name": "secubox-app-domoticz", "version": "1.0.0-r4", "filename": "secubox-app-domoticz_1.0.0-r4_all.ipk", - "size": 7503, + "size": 7507, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1026,7 +1038,7 @@ "name": "secubox-app-exposure", "version": "1.0.0-r1", "filename": "secubox-app-exposure_1.0.0-r1_all.ipk", - "size": 9147, + "size": 9145, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1038,7 +1050,7 @@ "name": "secubox-app-gitea", "version": "1.0.0-r5", "filename": "secubox-app-gitea_1.0.0-r5_all.ipk", - "size": 9439, + "size": 9441, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1050,7 +1062,7 @@ "name": "secubox-app-gk2hub", "version": "0.1.0-r1", "filename": "secubox-app-gk2hub_0.1.0-r1_all.ipk", - "size": 4064, + "size": 4059, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1062,7 +1074,7 @@ "name": "secubox-app-glances", "version": "1.0.0-r1", "filename": "secubox-app-glances_1.0.0-r1_all.ipk", - "size": 6142, + "size": 6137, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1074,7 +1086,7 @@ "name": "secubox-app-guacamole", "version": "1.0.0-r1", "filename": "secubox-app-guacamole_1.0.0-r1_all.ipk", - "size": 6948, + "size": 6945, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1086,7 +1098,7 @@ "name": "secubox-app-haproxy", "version": "1.0.0-r24", "filename": "secubox-app-haproxy_1.0.0-r24_all.ipk", - "size": 22012, + "size": 22008, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1098,7 +1110,7 @@ "name": "secubox-app-hexojs", "version": "1.0.0-r8", "filename": "secubox-app-hexojs_1.0.0-r8_all.ipk", - "size": 94937, + "size": 100060, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1110,7 +1122,7 @@ "name": "secubox-app-jabber", "version": "1.0.0-r1", "filename": "secubox-app-jabber_1.0.0-r1_all.ipk", - "size": 13277, + "size": 13276, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1122,7 +1134,7 @@ "name": "secubox-app-jellyfin", "version": "3.0.0-r1", "filename": "secubox-app-jellyfin_3.0.0-r1_all.ipk", - "size": 4748, + "size": 4755, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1134,7 +1146,7 @@ "name": "secubox-app-jitsi", "version": "1.0.0-r1", "filename": "secubox-app-jitsi_1.0.0-r1_all.ipk", - "size": 8927, + "size": 8924, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1146,7 +1158,7 @@ "name": "secubox-app-localai", "version": "3.9.0-r1", "filename": "secubox-app-localai_3.9.0-r1_all.ipk", - "size": 5848, + "size": 5841, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1170,7 +1182,7 @@ "name": "secubox-app-lyrion", "version": "2.0.2-r1", "filename": "secubox-app-lyrion_2.0.2-r1_all.ipk", - "size": 8130, + "size": 8124, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1182,7 +1194,7 @@ "name": "secubox-app-mac-guardian", "version": "0.5.0-r1", "filename": "secubox-app-mac-guardian_0.5.0-r1_all.ipk", - "size": 12098, + "size": 12097, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1206,7 +1218,7 @@ "name": "secubox-app-mailinabox", "version": "2.0.0-r1", "filename": "secubox-app-mailinabox_2.0.0-r1_all.ipk", - "size": 7569, + "size": 7574, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1218,7 +1230,7 @@ "name": "secubox-app-mailserver", "version": "2.0.0-r1", "filename": "secubox-app-mailserver_2.0.0-r1_all.ipk", - "size": 5697, + "size": 5693, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1242,7 +1254,7 @@ "name": "secubox-app-metabolizer", "version": "1.0.0-r3", "filename": "secubox-app-metabolizer_1.0.0-r3_all.ipk", - "size": 13980, + "size": 13975, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1254,7 +1266,7 @@ "name": "secubox-app-mitmproxy", "version": "0.5.0-r19", "filename": "secubox-app-mitmproxy_0.5.0-r19_all.ipk", - "size": 22958, + "size": 22957, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1266,7 +1278,7 @@ "name": "secubox-app-mmpm", "version": "0.2.0-r5", "filename": "secubox-app-mmpm_0.2.0-r5_all.ipk", - "size": 3978, + "size": 3979, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1290,7 +1302,7 @@ "name": "secubox-app-ollama", "version": "0.1.0-r1", "filename": "secubox-app-ollama_0.1.0-r1_all.ipk", - "size": 5735, + "size": 5736, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1314,7 +1326,7 @@ "name": "secubox-app-rustdesk", "version": "1.0.0-r1", "filename": "secubox-app-rustdesk_1.0.0-r1_all.ipk", - "size": 4469, + "size": 4467, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1326,7 +1338,7 @@ "name": "secubox-app-simplex", "version": "1.0.0-r1", "filename": "secubox-app-simplex_1.0.0-r1_all.ipk", - "size": 9369, + "size": 9367, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1338,7 +1350,7 @@ "name": "secubox-app-smbfs", "version": "1.0.0-r1", "filename": "secubox-app-smbfs_1.0.0-r1_all.ipk", - "size": 5269, + "size": 5268, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1350,7 +1362,7 @@ "name": "secubox-app-streamlit", "version": "1.0.0-r5", "filename": "secubox-app-streamlit_1.0.0-r5_all.ipk", - "size": 16517, + "size": 16519, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1362,7 +1374,7 @@ "name": "secubox-app-tor", "version": "1.0.0-r1", "filename": "secubox-app-tor_1.0.0-r1_all.ipk", - "size": 7372, + "size": 7370, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1374,7 +1386,7 @@ "name": "secubox-app-voip", "version": "1.0.0-r1", "filename": "secubox-app-voip_1.0.0-r1_all.ipk", - "size": 11955, + "size": 11954, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1386,7 +1398,7 @@ "name": "secubox-app-webapp", "version": "1.5.0-r7", "filename": "secubox-app-webapp_1.5.0-r7_all.ipk", - "size": 39176, + "size": 39177, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1398,7 +1410,7 @@ "name": "secubox-app-zigbee2mqtt", "version": "1.0.0-r3", "filename": "secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk", - "size": 5533, + "size": 5539, "category": "secubox", "icon": "package", "description": "SecuBox backend service", @@ -1410,7 +1422,7 @@ "name": "secubox-config-advisor", "version": "0.1.0-r1", "filename": "secubox-config-advisor_0.1.0-r1_all.ipk", - "size": 14850, + "size": 14849, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1422,7 +1434,7 @@ "name": "secubox-content-pkg", "version": "1.0.0-r1", "filename": "secubox-content-pkg_1.0.0-r1_all.ipk", - "size": 3914, + "size": 3904, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1434,7 +1446,7 @@ "name": "secubox-cookie-tracker", "version": "1.0.0-r1", "filename": "secubox-cookie-tracker_1.0.0-r1_all.ipk", - "size": 10645, + "size": 10641, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1446,7 +1458,7 @@ "name": "secubox-core", "version": "0.10.0-r16", "filename": "secubox-core_0.10.0-r16_all.ipk", - "size": 123047, + "size": 123045, "category": "system", "icon": "box", "description": "SecuBox core components", @@ -1458,7 +1470,7 @@ "name": "secubox-cve-triage", "version": "1.0.0-r1", "filename": "secubox-cve-triage_1.0.0-r1_all.ipk", - "size": 11826, + "size": 11827, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1470,7 +1482,7 @@ "name": "secubox-dns-guard", "version": "1.0.0-r1", "filename": "secubox-dns-guard_1.0.0-r1_all.ipk", - "size": 12488, + "size": 12485, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1482,7 +1494,7 @@ "name": "secubox-identity", "version": "0.1.0-r1", "filename": "secubox-identity_0.1.0-r1_all.ipk", - "size": 8089, + "size": 8088, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1494,7 +1506,7 @@ "name": "secubox-iot-guard", "version": "1.0.0-r1", "filename": "secubox-iot-guard_1.0.0-r1_all.ipk", - "size": 13374, + "size": 13365, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1506,7 +1518,7 @@ "name": "secubox-localrecall", "version": "1.0.0-r1", "filename": "secubox-localrecall_1.0.0-r1_all.ipk", - "size": 7802, + "size": 7796, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1518,7 +1530,7 @@ "name": "secubox-master-link", "version": "1.0.0-r1", "filename": "secubox-master-link_1.0.0-r1_all.ipk", - "size": 15038, + "size": 15034, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1530,7 +1542,7 @@ "name": "secubox-mcp-server", "version": "1.0.0-r1", "filename": "secubox-mcp-server_1.0.0-r1_all.ipk", - "size": 11433, + "size": 11427, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1542,7 +1554,7 @@ "name": "secubox-mirrornet", "version": "0.1.0-r1", "filename": "secubox-mirrornet_0.1.0-r1_all.ipk", - "size": 15306, + "size": 15303, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1554,7 +1566,7 @@ "name": "secubox-network-anomaly", "version": "1.0.0-r1", "filename": "secubox-network-anomaly_1.0.0-r1_all.ipk", - "size": 6170, + "size": 6163, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1566,7 +1578,7 @@ "name": "secubox-p2p", "version": "0.6.0-r3", "filename": "secubox-p2p_0.6.0-r3_all.ipk", - "size": 47872, + "size": 47860, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1578,7 +1590,7 @@ "name": "secubox-p2p-intel", "version": "0.1.0-r1", "filename": "secubox-p2p-intel_0.1.0-r1_all.ipk", - "size": 9798, + "size": 9799, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1590,7 +1602,7 @@ "name": "secubox-threat-analyst", "version": "1.0.0-r1", "filename": "secubox-threat-analyst_1.0.0-r1_all.ipk", - "size": 9867, + "size": 9864, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1602,7 +1614,7 @@ "name": "secubox-vortex-dns", "version": "1.0.0-r1", "filename": "secubox-vortex-dns_1.0.0-r1_all.ipk", - "size": 5443, + "size": 5439, "category": "utility", "icon": "package", "description": "SecuBox package", @@ -1614,7 +1626,7 @@ "name": "secubox-vortex-firewall", "version": "1.0.0-r1", "filename": "secubox-vortex-firewall_1.0.0-r1_all.ipk", - "size": 8898, + "size": 8892, "category": "utility", "icon": "package", "description": "SecuBox package", diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ai-insights_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ai-insights_1.0.0-r1_all.ipk index 2e53e32a..0a961b41 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ai-insights_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ai-insights_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-auth-guardian_0.4.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-auth-guardian_0.4.0-r3_all.ipk index 3a6cc518..a234fe75 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-auth-guardian_0.4.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-auth-guardian_0.4.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-backup_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-backup_1.0.0-r1_all.ipk index 24f9c179..b719686c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-backup_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-backup_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-bandwidth-manager_0.5.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-bandwidth-manager_0.5.0-r2_all.ipk index f60711a1..eea23132 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-bandwidth-manager_0.5.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-bandwidth-manager_0.5.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cdn-cache_0.5.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cdn-cache_0.5.0-r3_all.ipk index 12afbb03..1abfb41b 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cdn-cache_0.5.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cdn-cache_0.5.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-client-guardian_0.4.0-r7_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-client-guardian_0.4.0-r7_all.ipk index 4841eef7..95635528 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-client-guardian_0.4.0-r7_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-client-guardian_0.4.0-r7_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cloner_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cloner_1.0.0-r1_all.ipk index 37c1c8c4..ede88b7f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cloner_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cloner_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-config-advisor_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-config-advisor_1.0.0-r1_all.ipk index dd2d2130..2042bb56 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-config-advisor_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-config-advisor_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cookie-tracker_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cookie-tracker_1.0.0-r1_all.ipk index 7c3a04d7..2d97f1f8 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cookie-tracker_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cookie-tracker_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk index bf82b294..56cf3bcf 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-crowdsec-dashboard_0.7.0-r32_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cve-triage_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cve-triage_1.0.0-r1_all.ipk index 6130f242..02fbaedb 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cve-triage_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cve-triage_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cyberfeed_0.1.1-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cyberfeed_0.1.1-r1_all.ipk index 4e478b48..069edacb 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cyberfeed_0.1.1-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-cyberfeed_0.1.1-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-device-intel_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-device-intel_1.0.0-r1_all.ipk index d8ad4ca0..31b27e98 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-device-intel_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-device-intel_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dns-provider_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dns-provider_1.0.0-r1_all.ipk index cba86520..028bbfb1 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dns-provider_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dns-provider_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dnsguard_1.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dnsguard_1.1.0-r1_all.ipk index b0c80406..be361a4d 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dnsguard_1.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-dnsguard_1.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-domoticz_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-domoticz_1.0.0-r1_all.ipk index bb85cb8f..baf9e97f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-domoticz_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-domoticz_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-exposure_1.0.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-exposure_1.0.0-r3_all.ipk index d98e81ef..af57507f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-exposure_1.0.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-exposure_1.0.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gitea_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gitea_1.0.0-r2_all.ipk index 9d098bc6..1cc39da2 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gitea_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gitea_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-glances_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-glances_1.0.0-r2_all.ipk index 78e7ff35..0fd80f8a 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-glances_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-glances_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gotosocial_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gotosocial_0.1.0-r1_all.ipk index b77cbb1c..35330355 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gotosocial_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-gotosocial_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-haproxy_1.0.0-r8_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-haproxy_1.0.0-r8_all.ipk index db4cf1c2..19e25c2c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-haproxy_1.0.0-r8_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-haproxy_1.0.0-r8_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-hexojs_1.0.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-hexojs_1.0.0-r3_all.ipk index 1644ac0d..3ffaae8e 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-hexojs_1.0.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-hexojs_1.0.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-iot-guard_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-iot-guard_1.0.0-r1_all.ipk index 7d0ee7fe..191067b8 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-iot-guard_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-iot-guard_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jabber_0_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jabber_0_all.ipk index d6561ad8..e74e7553 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jabber_0_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jabber_0_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jellyfin_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jellyfin_1.0.0-r1_all.ipk index 5fc1e472..6f8cf5b2 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jellyfin_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jellyfin_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jitsi_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jitsi_1.0.0-r1_all.ipk index 26c4e61f..1bfb3dcc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jitsi_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-jitsi_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ksm-manager_0.4.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ksm-manager_0.4.0-r2_all.ipk index a9579824..ac29ea07 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ksm-manager_0.4.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ksm-manager_0.4.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localai_0.1.0-r15_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localai_0.1.0-r15_all.ipk index d5c4c9bd..c88cbd9b 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localai_0.1.0-r15_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localai_0.1.0-r15_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localrecall_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localrecall_1.0.0-r1_all.ipk index ac24d1fc..93d23807 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localrecall_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-localrecall_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-lyrion_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-lyrion_1.0.0-r1_all.ipk index 6b4d71df..81dc2fce 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-lyrion_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-lyrion_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mac-guardian_0.5.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mac-guardian_0.5.0-r1_all.ipk index 17e1b248..a781ade7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mac-guardian_0.5.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mac-guardian_0.5.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-magicmirror2_0.4.0-r6_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-magicmirror2_0.4.0-r6_all.ipk index a74ea290..edec06e4 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-magicmirror2_0.4.0-r6_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-magicmirror2_0.4.0-r6_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailinabox_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailinabox_1.0.0-r1_all.ipk index dd16fa0e..87ce71cf 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailinabox_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailinabox_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailserver_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailserver_1.0.0-r1_all.ipk index 8728b07e..c973d722 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailserver_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mailserver_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-master-link_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-master-link_1.0.0-r1_all.ipk index f7a3d293..41370a52 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-master-link_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-master-link_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-matrix_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-matrix_1.0.0-r1_all.ipk index a682a53b..d723eb69 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-matrix_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-matrix_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-media-flow_0.6.4-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-media-flow_0.6.4-r1_all.ipk index a7e03579..dcf2ffda 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-media-flow_0.6.4-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-media-flow_0.6.4-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metablogizer_1.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metablogizer_1.1.0-r1_all.ipk index d766b117..ed752e5f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metablogizer_1.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metablogizer_1.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metabolizer_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metabolizer_1.0.0-r2_all.ipk index 2e4911b5..713f820e 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metabolizer_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-metabolizer_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mitmproxy_0.5.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mitmproxy_0.5.0-r2_all.ipk index 7fabfde4..41e0c79d 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mitmproxy_0.5.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mitmproxy_0.5.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mmpm_0.2.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mmpm_0.2.0-r3_all.ipk index 2408acb3..c7873e05 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mmpm_0.2.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mmpm_0.2.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mqtt-bridge_0.4.0-r4_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mqtt-bridge_0.4.0-r4_all.ipk index 2ed375b1..a25ac251 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mqtt-bridge_0.4.0-r4_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-mqtt-bridge_0.4.0-r4_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ndpid_1.1.2-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ndpid_1.1.2-r2_all.ipk index 37a9bee5..2ff073a5 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ndpid_1.1.2-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ndpid_1.1.2-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-netdata-dashboard_0.5.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-netdata-dashboard_0.5.0-r2_all.ipk index 3975fe40..ae1e00b1 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-netdata-dashboard_0.5.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-netdata-dashboard_0.5.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-anomaly_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-anomaly_1.0.0-r1_all.ipk index 1d412c06..d824cbb6 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-anomaly_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-anomaly_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-modes_0.5.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-modes_0.5.0-r3_all.ipk index 57e9aacb..c6dae8e2 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-modes_0.5.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-modes_0.5.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-tweaks_1.0.0-r7_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-tweaks_1.0.0-r7_all.ipk index 6e7bb45c..809f2989 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-tweaks_1.0.0-r7_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-network-tweaks_1.0.0-r7_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-nextcloud_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-nextcloud_1.0.0-r1_all.ipk index a41ce5b8..fc0ef290 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-nextcloud_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-nextcloud_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ollama_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ollama_0.1.0-r1_all.ipk index 2b26a827..4a72f655 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ollama_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-ollama_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-peertube_0_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-peertube_0_all.ipk index 8bbe0084..b5ee53b7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-peertube_0_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-peertube_0_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-picobrew_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-picobrew_1.0.0-r1_all.ipk index c99d599d..b3e3a877 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-picobrew_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-picobrew_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-admin_1.0.0-r19_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-admin_1.0.0-r19_all.ipk index d96c04e7..5186e465 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-admin_1.0.0-r19_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-admin_1.0.0-r19_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-crowdsec_1.0.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-crowdsec_1.0.0-r3_all.ipk index 3bd58bb2..94b33b77 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-crowdsec_1.0.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-crowdsec_1.0.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-mirror_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-mirror_0.1.0-r1_all.ipk index 9acc9f7c..2a1b07f8 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-mirror_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-mirror_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netdiag_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netdiag_1.0.0-r1_all.ipk index f068bdc7..d1bda1d7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netdiag_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netdiag_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netifyd_1.2.1-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netifyd_1.2.1-r1_all.ipk index 7b63bfda..451419de 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netifyd_1.2.1-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-netifyd_1.2.1-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-p2p_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-p2p_0.1.0-r1_all.ipk index 795645fc..95f3f8ca 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-p2p_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-p2p_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-portal_0.7.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-portal_0.7.0-r3_all.ipk index 30e95a24..ee90d95a 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-portal_0.7.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-portal_0.7.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-security-threats_1.0.0-r4_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-security-threats_1.0.0-r4_all.ipk index 8ed47f70..088ba198 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-security-threats_1.0.0-r4_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-security-threats_1.0.0-r4_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-users_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-users_1.0.0-r1_all.ipk new file mode 100644 index 00000000..115a61c4 Binary files /dev/null and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-users_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox_0.7.1-r4_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox_0.7.1-r4_all.ipk index 4513df7e..70af9f6a 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox_0.7.1-r4_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox_0.7.1-r4_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-service-registry_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-service-registry_1.0.0-r1_all.ipk index e65f0960..5580a819 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-service-registry_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-service-registry_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-simplex_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-simplex_1.0.0-r1_all.ipk index 4db87f88..9bf8b5e8 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-simplex_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-simplex_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk index 1e80aea2..055774bf 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-streamlit_1.0.0-r11_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-system-hub_0.5.1-r4_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-system-hub_0.5.1-r4_all.ipk index 8ced8661..168de85b 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-system-hub_0.5.1-r4_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-system-hub_0.5.1-r4_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-threat-analyst_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-threat-analyst_1.0.0-r1_all.ipk index a9f29813..bfc4e640 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-threat-analyst_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-threat-analyst_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor-shield_1.0.0-r10_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor-shield_1.0.0-r10_all.ipk index b32283fc..7af4fc01 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor-shield_1.0.0-r10_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor-shield_1.0.0-r10_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor_1.0.0-r1_all.ipk index 805013f7..d6c62e09 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-tor_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-traffic-shaper_0.4.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-traffic-shaper_0.4.0-r2_all.ipk index 5735be74..93e76101 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-traffic-shaper_0.4.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-traffic-shaper_0.4.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vhost-manager_0.5.0-r5_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vhost-manager_0.5.0-r5_all.ipk index 1a32dcbe..d955ca73 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vhost-manager_0.5.0-r5_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vhost-manager_0.5.0-r5_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-voip_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-voip_1.0.0-r1_all.ipk index 0fa1d929..677af762 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-voip_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-voip_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-dns_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-dns_1.0.0-r1_all.ipk index 61ef1cd5..7efb2602 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-dns_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-dns_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-firewall_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-firewall_1.0.0-r1_all.ipk index 4baa430a..d3346164 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-firewall_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-vortex-firewall_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wazuh_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wazuh_1.0.0-r1_all.ipk index 8ddc3bab..be277d80 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wazuh_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wazuh_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wireguard-dashboard_0.7.0-r5_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wireguard-dashboard_0.7.0-r5_all.ipk index f7ceb8f2..555ae417 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wireguard-dashboard_0.7.0-r5_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-wireguard-dashboard_0.7.0-r5_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-zigbee2mqtt_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-zigbee2mqtt_1.0.0-r2_all.ipk index ca08c06a..77ec40c0 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-zigbee2mqtt_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-zigbee2mqtt_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-theme-secubox_0.4.7-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-theme-secubox_0.4.7-r1_all.ipk index 9fc3793d..e06e089a 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-theme-secubox_0.4.7-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-theme-secubox_0.4.7-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-adguardhome_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-adguardhome_1.0.0-r2_all.ipk index 7e4ae53f..5a62eff7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-adguardhome_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-adguardhome_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-auth-logger_1.2.2-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-auth-logger_1.2.2-r1_all.ipk index a7aeecca..a9885b32 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-auth-logger_1.2.2-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-auth-logger_1.2.2-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-crowdsec-custom_1.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-crowdsec-custom_1.1.0-r1_all.ipk index 153d0dce..b0ff96bc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-crowdsec-custom_1.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-crowdsec-custom_1.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk index d87a2951..7ba72231 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cs-firewall-bouncer_0.0.31-r4_aarch64_cortex-a72.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cyberfeed_0.2.1-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cyberfeed_0.2.1-r1_all.ipk index 688e7713..cea194e4 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cyberfeed_0.2.1-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-cyberfeed_0.2.1-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-device-intel_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-device-intel_1.0.0-r1_all.ipk index 8f367b6e..312e4f54 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-device-intel_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-device-intel_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-dns-provider_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-dns-provider_1.0.0-r1_all.ipk index 4ac8e082..98e2158c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-dns-provider_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-dns-provider_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-domoticz_1.0.0-r4_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-domoticz_1.0.0-r4_all.ipk index b9e20542..cd2bc792 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-domoticz_1.0.0-r4_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-domoticz_1.0.0-r4_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-exposure_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-exposure_1.0.0-r1_all.ipk index 5c43660e..38ee9faf 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-exposure_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-exposure_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gitea_1.0.0-r5_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gitea_1.0.0-r5_all.ipk index 68b73f27..5ddfa6a2 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gitea_1.0.0-r5_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gitea_1.0.0-r5_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gk2hub_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gk2hub_0.1.0-r1_all.ipk index 95e09d7b..8db3c69c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gk2hub_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-gk2hub_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-glances_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-glances_1.0.0-r1_all.ipk index 918692ea..76e452ba 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-glances_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-glances_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-guacamole_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-guacamole_1.0.0-r1_all.ipk index 8557fcd3..790d2bb0 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-guacamole_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-guacamole_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-haproxy_1.0.0-r24_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-haproxy_1.0.0-r24_all.ipk index a3ae429e..0f17c93a 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-haproxy_1.0.0-r24_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-haproxy_1.0.0-r24_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-hexojs_1.0.0-r8_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-hexojs_1.0.0-r8_all.ipk index 1c1cdcf2..0f98b1c0 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-hexojs_1.0.0-r8_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-hexojs_1.0.0-r8_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jabber_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jabber_1.0.0-r1_all.ipk index e811e0b6..bcde29fc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jabber_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jabber_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jellyfin_3.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jellyfin_3.0.0-r1_all.ipk index d82787c3..385ff8c2 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jellyfin_3.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jellyfin_3.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jitsi_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jitsi_1.0.0-r1_all.ipk index 107a5757..344aa9d6 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jitsi_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-jitsi_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai-wb_2.25.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai-wb_2.25.0-r1_all.ipk index 6fef5626..35ffcacf 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai-wb_2.25.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai-wb_2.25.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai_3.9.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai_3.9.0-r1_all.ipk index e079ed17..2bfb4eb4 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai_3.9.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-localai_3.9.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-lyrion_2.0.2-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-lyrion_2.0.2-r1_all.ipk index 88635283..811a9012 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-lyrion_2.0.2-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-lyrion_2.0.2-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mac-guardian_0.5.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mac-guardian_0.5.0-r1_all.ipk index d246c763..e600ca3b 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mac-guardian_0.5.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mac-guardian_0.5.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-magicmirror2_0.4.0-r8_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-magicmirror2_0.4.0-r8_all.ipk index d04ef6e1..1afe3c7f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-magicmirror2_0.4.0-r8_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-magicmirror2_0.4.0-r8_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailinabox_2.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailinabox_2.0.0-r1_all.ipk index 80dc31e1..467816d3 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailinabox_2.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailinabox_2.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailserver_2.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailserver_2.0.0-r1_all.ipk index d1bdf482..11bbd7cc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailserver_2.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mailserver_2.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-matrix_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-matrix_1.0.0-r1_all.ipk index 869ba04e..fc5d4bf7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-matrix_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-matrix_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-metabolizer_1.0.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-metabolizer_1.0.0-r3_all.ipk index e0bdf38a..e106eebb 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-metabolizer_1.0.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-metabolizer_1.0.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mitmproxy_0.5.0-r19_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mitmproxy_0.5.0-r19_all.ipk index 68e8fd9d..cffc6c8d 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mitmproxy_0.5.0-r19_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mitmproxy_0.5.0-r19_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mmpm_0.2.0-r5_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mmpm_0.2.0-r5_all.ipk index c4e53de0..0378ac50 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mmpm_0.2.0-r5_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-mmpm_0.2.0-r5_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-nextcloud_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-nextcloud_1.0.0-r2_all.ipk index 3e6ec87a..56b876e7 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-nextcloud_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-nextcloud_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ollama_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ollama_0.1.0-r1_all.ipk index 5301d053..48d9b453 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ollama_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ollama_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-picobrew_1.0.0-r7_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-picobrew_1.0.0-r7_all.ipk index e8520d45..8fb56fcc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-picobrew_1.0.0-r7_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-picobrew_1.0.0-r7_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-rustdesk_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-rustdesk_1.0.0-r1_all.ipk index 8aec3056..107c74cc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-rustdesk_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-rustdesk_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-simplex_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-simplex_1.0.0-r1_all.ipk index 76e5d78e..fb1a37fd 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-simplex_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-simplex_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-smbfs_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-smbfs_1.0.0-r1_all.ipk index ff0ef2a5..64466232 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-smbfs_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-smbfs_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-streamlit_1.0.0-r5_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-streamlit_1.0.0-r5_all.ipk index 172040c0..ffb00ec6 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-streamlit_1.0.0-r5_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-streamlit_1.0.0-r5_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-tor_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-tor_1.0.0-r1_all.ipk index f07cbfd4..517ec976 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-tor_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-tor_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-voip_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-voip_1.0.0-r1_all.ipk index 5c5965f2..7c5211ca 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-voip_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-voip_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-webapp_1.5.0-r7_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-webapp_1.5.0-r7_all.ipk index ba1af927..a960b3b6 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-webapp_1.5.0-r7_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-webapp_1.5.0-r7_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk index 3f12b466..9068d8ab 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-zigbee2mqtt_1.0.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app_1.0.0-r2_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app_1.0.0-r2_all.ipk index 7a7a09c8..24809263 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app_1.0.0-r2_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app_1.0.0-r2_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-config-advisor_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-config-advisor_0.1.0-r1_all.ipk index f5ef3e98..ea50d43d 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-config-advisor_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-config-advisor_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-content-pkg_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-content-pkg_1.0.0-r1_all.ipk index 583c8a57..fbbbb5c3 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-content-pkg_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-content-pkg_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cookie-tracker_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cookie-tracker_1.0.0-r1_all.ipk index 39636c0c..26972ced 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cookie-tracker_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cookie-tracker_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-core_0.10.0-r16_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-core_0.10.0-r16_all.ipk index 7d0c44fe..99a6ada3 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-core_0.10.0-r16_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-core_0.10.0-r16_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cve-triage_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cve-triage_1.0.0-r1_all.ipk index a6b810c2..1c4a3b7c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cve-triage_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-cve-triage_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-dns-guard_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-dns-guard_1.0.0-r1_all.ipk index 04449130..1c8ae08f 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-dns-guard_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-dns-guard_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-identity_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-identity_0.1.0-r1_all.ipk index 7e23430a..3577a1dc 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-identity_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-identity_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-iot-guard_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-iot-guard_1.0.0-r1_all.ipk index 258ef48d..dde7968e 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-iot-guard_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-iot-guard_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-localrecall_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-localrecall_1.0.0-r1_all.ipk index 43ab1484..4ad60f79 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-localrecall_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-localrecall_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-master-link_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-master-link_1.0.0-r1_all.ipk index 34753060..f9cf3e78 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-master-link_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-master-link_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mcp-server_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mcp-server_1.0.0-r1_all.ipk index 43e17bc2..9c32ca3c 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mcp-server_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mcp-server_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mirrornet_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mirrornet_0.1.0-r1_all.ipk index 17f34893..7ca6eef5 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mirrornet_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-mirrornet_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-network-anomaly_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-network-anomaly_1.0.0-r1_all.ipk index bb9a7eef..3e051f44 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-network-anomaly_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-network-anomaly_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p-intel_0.1.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p-intel_0.1.0-r1_all.ipk index 11738797..6cc25614 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p-intel_0.1.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p-intel_0.1.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p_0.6.0-r3_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p_0.6.0-r3_all.ipk index ae3d1ece..eea90f8e 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p_0.6.0-r3_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-p2p_0.6.0-r3_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-threat-analyst_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-threat-analyst_1.0.0-r1_all.ipk index 9739c383..bdc73302 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-threat-analyst_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-threat-analyst_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-dns_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-dns_1.0.0-r1_all.ipk index feb593a4..77a68404 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-dns_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-dns_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-firewall_1.0.0-r1_all.ipk b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-firewall_1.0.0-r1_all.ipk index 2d335dbc..992380ed 100644 Binary files a/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-firewall_1.0.0-r1_all.ipk and b/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-vortex-firewall_1.0.0-r1_all.ipk differ diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index 77ebdba4..6670c050 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -535,6 +535,12 @@ defaults EOF + # SecuBox userlist (for private vhosts) + if [ -f "$AUTH_USERLIST_FILE" ]; then + cat "$AUTH_USERLIST_FILE" >> "$cfg_file" + echo "" >> "$cfg_file" + fi + # Stats frontend if [ "$stats_enabled" = "1" ]; then cat >> "$cfg_file" << EOF @@ -787,6 +793,16 @@ _add_vhost_acl() { echo " acl host_${acl_name} hdr(host) -i $domain" ;; esac + + # Add auth requirement for private vhosts + local auth_enabled + config_get auth_enabled "$section" auth_enabled "0" + if [ "$auth_enabled" = "1" ]; then + echo " http-request auth realm \"SecuBox\" if host_${acl_name} !{ http_auth(secubox_users) }" + # Log auth user in header for backend (http_auth_user only works after successful auth) + echo " http-request set-header X-Auth-User %[http_auth_user] if host_${acl_name}" + fi + # Use WAF backend if enabled, otherwise use original backend local effective_backend="$backend" config_get waf_bypass "$section" waf_bypass "0" @@ -2043,6 +2059,172 @@ cmd_path_clear() { fi } +# =========================================== +# Authentication Management +# =========================================== + +AUTH_USERLIST_FILE="$CONFIG_PATH/secubox-users.cfg" + +# Sync SecuBox/LuCI users to HAProxy userlist +cmd_auth_sync() { + log_info "Syncing SecuBox users to HAProxy userlist..." + + local tmpfile="/tmp/haproxy_users.$$" + local count=0 + + echo "# SecuBox Users - Auto-generated" > "$tmpfile" + echo "# Do not edit - regenerated by haproxyctl auth sync" >> "$tmpfile" + echo "" >> "$tmpfile" + echo "userlist secubox_users" >> "$tmpfile" + + # Iterate through rpcd login users + for section in $(uci show rpcd 2>/dev/null | grep '=login' | cut -d'.' -f2 | cut -d'=' -f1); do + local username=$(uci -q get rpcd.${section}.username) + [ -z "$username" ] && continue + + local pw_ref=$(uci -q get rpcd.${section}.password) + local hash="" + + # Handle $p$username reference to /etc/shadow + if echo "$pw_ref" | grep -q '^\$p\$'; then + local sys_user="${pw_ref#\$p\$}" + local shadow_hash=$(grep "^${sys_user}:" /etc/shadow 2>/dev/null | cut -d: -f2) + if [ -n "$shadow_hash" ] && [ "$shadow_hash" != "*" ] && [ "$shadow_hash" != "x" ]; then + hash="$shadow_hash" + fi + else + hash="$pw_ref" + fi + + if [ -n "$hash" ]; then + echo " user $username password $hash" >> "$tmpfile" + count=$((count + 1)) + log_info " Added: $username" + else + log_warn " Skip: $username (no password hash)" + fi + done + + # Move to final location + mv "$tmpfile" "$AUTH_USERLIST_FILE" + chmod 600 "$AUTH_USERLIST_FILE" + + log_info "Synced $count users to $AUTH_USERLIST_FILE" + + # Regenerate config to include userlist + generate_config + cmd_reload + + log_info "HAProxy reloaded with updated userlist" +} + +# Enable auth for a vhost (make it private) +cmd_auth_enable() { + local domain="$1" + [ -z "$domain" ] && { echo "Usage: haproxyctl auth enable "; return 1; } + + # Find vhost section + local section=$(echo "$domain" | tr '.' '_' | tr '-' '_') + section="vhost_${section}" + + local existing=$(uci -q get haproxy.${section}.domain) + if [ -z "$existing" ]; then + log_error "Vhost not found: $domain" + return 1 + fi + + uci set haproxy.${section}.auth_enabled='1' + uci commit haproxy + + log_info "Auth enabled for $domain (private mode)" + + # Regenerate config + generate_config + cmd_reload + + log_info "To sync users: haproxyctl auth sync" +} + +# Disable auth for a vhost (make it public) +cmd_auth_disable() { + local domain="$1" + [ -z "$domain" ] && { echo "Usage: haproxyctl auth disable "; return 1; } + + local section=$(echo "$domain" | tr '.' '_' | tr '-' '_') + section="vhost_${section}" + + local existing=$(uci -q get haproxy.${section}.domain) + if [ -z "$existing" ]; then + log_error "Vhost not found: $domain" + return 1 + fi + + uci set haproxy.${section}.auth_enabled='0' + uci commit haproxy + + log_info "Auth disabled for $domain (public mode)" + + generate_config + cmd_reload +} + +# Show auth status for all vhosts +cmd_auth_status() { + echo "HAProxy Authentication Status" + echo "==============================" + echo "" + + # Check if userlist exists + if [ -f "$AUTH_USERLIST_FILE" ]; then + local user_count=$(grep -c "^ user " "$AUTH_USERLIST_FILE" 2>/dev/null || echo 0) + echo "Userlist: $AUTH_USERLIST_FILE ($user_count users)" + echo "" + else + echo "Userlist: Not configured (run: haproxyctl auth sync)" + echo "" + fi + + echo "Vhosts:" + echo "-------" + + config_load haproxy + _print_auth_status() { + local section="$1" + local enabled domain auth_enabled + + config_get enabled "$section" enabled "0" + [ "$enabled" = "1" ] || return + + config_get domain "$section" domain + config_get auth_enabled "$section" auth_enabled "0" + + [ -n "$domain" ] || return + + if [ "$auth_enabled" = "1" ]; then + printf " %-35s [PRIVATE] (auth required)\n" "$domain" + else + printf " %-35s [PUBLIC]\n" "$domain" + fi + } + config_foreach _print_auth_status vhost +} + +# Show authentication log +cmd_auth_log() { + local lines="${1:-50}" + echo "Authentication Log (last $lines entries)" + echo "=========================================" + logread | grep -E "haproxy.*auth|HAProxy.*401|haproxy.*unauthorized" | tail -n "$lines" +} + +# Generate userlist include for haproxy.cfg +_generate_userlist() { + if [ -f "$AUTH_USERLIST_FILE" ]; then + cat "$AUTH_USERLIST_FILE" + echo "" + fi +} + # =========================================== # Main # =========================================== @@ -2119,6 +2301,18 @@ case "${1:-}" in service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; + auth) + shift + case "${1:-}" in + sync) shift; cmd_auth_sync "$@" ;; + enable) shift; cmd_auth_enable "$@" ;; + disable) shift; cmd_auth_disable "$@" ;; + status) shift; cmd_auth_status "$@" ;; + log) shift; cmd_auth_log "$@" ;; + *) echo "Usage: haproxyctl auth {sync|enable|disable|status|log}" ;; + esac + ;; + shell) shift; lxc_exec /bin/sh ;; exec) shift; lxc_exec "$@" ;; diff --git a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs index e392d57e..f2f19b4a 100644 --- a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs +++ b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs @@ -42,3 +42,33 @@ config theme_config 'theme' config portal 'portal' option enabled '0' option path '/www' + +# Authentication settings for protected sites +config auth 'auth' + option enabled '0' + option method 'basic' + # basic = HTTP Basic Auth via HAProxy + # secubox = SecuBox LuCI user integration + option realm 'SecuBox HexoJS' + option htpasswd_path '/etc/hexojs/htpasswd' + +# Example instance with authentication +# config instance 'private_blog' +# option enabled '1' +# option port '4001' +# option title 'Private Blog' +# option theme 'cybermind' +# option auth_enabled '1' +# option auth_users 'admin,editor1' +# option owner 'admin' + +# Example user +# config user 'admin' +# option password_hash '$apr1$...' +# option role 'owner' +# list instances 'private_blog' + +# config user 'editor1' +# option password_hash '$apr1$...' +# option role 'editor' +# list instances 'private_blog' diff --git a/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl b/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl index 346506c5..44e0b6a2 100644 --- a/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl +++ b/package/secubox/secubox-app-hexojs/files/usr/sbin/hexoctl @@ -176,6 +176,31 @@ Backup/Restore: Quick Commands: quick-publish [instance] Clean, build, and publish +User Management: + user add [password] Add user with password + user del Delete user + user list List all users + user passwd [pass] Change user password + user grant Grant access to instance + user revoke Revoke access to instance + user sync-secubox Import users from SecuBox/LuCI + +Authentication: + auth enable Enable auth for instance + auth disable Disable auth for instance + auth status [instance] Show auth status + auth haproxy Generate HAProxy auth ACLs + auth apply [domain] Auto-configure HAProxy with auth + +KISS Static Sites (No Hexo Build): + static create Create static-only site + static upload [inst] Upload HTML/CSS/JS directly + static publish [instance] Publish to /www/ for immediate serving + static quick [inst] Upload + publish in one step + static list [instance] List static files + static serve [instance] Serve static files (Python/httpd) + static delete Delete static instance + Utility: shell Open shell in container logs [instance] View logs @@ -1440,6 +1465,682 @@ EOF echo "}" } +# =============================================== +# User Management Commands +# =============================================== + +HTPASSWD_FILE="/etc/hexojs/htpasswd" + +# Generate password hash (apr1 format for HAProxy) +generate_password_hash() { + local password="$1" + # Use openssl for apr1 hash + openssl passwd -apr1 "$password" 2>/dev/null || \ + openssl passwd -1 "$password" 2>/dev/null || \ + echo "$password" # fallback to plain (not recommended) +} + +cmd_user_add() { + require_root + + local username="$1" + local password="$2" + + [ -z "$username" ] && { log_error "Username required"; return 1; } + + # Validate username + echo "$username" | grep -qE '^[a-z][a-z0-9_]*$' || { + log_error "Invalid username. Use lowercase letters, numbers, underscore." + return 1 + } + + # Check if user exists + local existing=$(uci_get "user_$username") + [ -n "$existing" ] && { log_error "User '$username' already exists"; return 1; } + + # Generate password if not provided + [ -z "$password" ] && { + password=$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 12) + log_info "Generated password: $password" + } + + # Create password hash + local hash=$(generate_password_hash "$password") + + # Create UCI user section + uci set hexojs.user_${username}=user + uci set hexojs.user_${username}.password_hash="$hash" + uci set hexojs.user_${username}.role='editor' + uci commit hexojs + + # Update htpasswd file + cmd_update_htpasswd + + log_info "User '$username' created" +} + +cmd_user_del() { + require_root + + local username="$1" + [ -z "$username" ] && { log_error "Username required"; return 1; } + + # Remove from UCI + uci delete hexojs.user_${username} 2>/dev/null || { + log_error "User '$username' not found" + return 1 + } + uci commit hexojs + + # Update htpasswd file + cmd_update_htpasswd + + log_info "User '$username' deleted" +} + +# Sync users from SecuBox/LuCI (rpcd config) +cmd_user_sync_secubox() { + require_root + + log_info "Syncing users from SecuBox/LuCI..." + + local added=0 + local skipped=0 + + # Read all login sections from rpcd config + for section in $(uci show rpcd 2>/dev/null | grep '=login$' | cut -d'.' -f2 | cut -d'=' -f1); do + local username=$(uci -q get rpcd.${section}.username) + [ -z "$username" ] && continue + + # Skip if already exists in hexojs + local existing=$(uci_get "user_$username" 2>/dev/null) + if [ -n "$existing" ]; then + log_info " Skip: $username (already exists)" + skipped=$((skipped + 1)) + continue + fi + + # Get password - format is $p$ or direct hash + local pw_ref=$(uci -q get rpcd.${section}.password) + + # If $p$username format, get hash from /etc/shadow + local hash="" + if echo "$pw_ref" | grep -q '^\$p\$'; then + local sys_user="${pw_ref#\$p\$}" + # Extract hash from shadow file + local shadow_hash=$(grep "^${sys_user}:" /etc/shadow 2>/dev/null | cut -d: -f2) + if [ -n "$shadow_hash" ] && [ "$shadow_hash" != "*" ] && [ "$shadow_hash" != "x" ]; then + hash="$shadow_hash" + fi + else + # Direct hash in rpcd config + hash="$pw_ref" + fi + + if [ -z "$hash" ]; then + log_warn " Skip: $username (no password hash found)" + skipped=$((skipped + 1)) + continue + fi + + # Create HexoJS user with the same hash + uci set hexojs.user_${username}=user + uci set hexojs.user_${username}.password_hash="$hash" + uci set hexojs.user_${username}.role='editor' + uci set hexojs.user_${username}.source='secubox' + + log_info " Added: $username" + added=$((added + 1)) + done + + uci commit hexojs + + # Update htpasswd file + cmd_update_htpasswd + + log_info "Sync complete: $added added, $skipped skipped" +} + +cmd_user_list() { + load_config + + echo "HexoJS Users:" + echo "-------------" + + local found=0 + for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do + found=1 + local username="${section#user_}" + local role=$(uci_get "${section}.role") + local instances=$(uci_get "${section}.instances") + + printf " %-15s role:%-8s instances: %s\n" "$username" "${role:-editor}" "${instances:-all}" + done + + [ "$found" = "0" ] && echo " (no users)" +} + +cmd_user_passwd() { + require_root + + local username="$1" + local password="$2" + + [ -z "$username" ] && { log_error "Username required"; return 1; } + + # Check if user exists + local existing=$(uci_get "user_$username") + [ -z "$existing" ] && { log_error "User '$username' not found"; return 1; } + + # Generate password if not provided + [ -z "$password" ] && { + password=$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 12) + log_info "Generated password: $password" + } + + # Create password hash + local hash=$(generate_password_hash "$password") + + # Update UCI + uci set hexojs.user_${username}.password_hash="$hash" + uci commit hexojs + + # Update htpasswd file + cmd_update_htpasswd + + log_info "Password changed for '$username'" +} + +cmd_user_grant() { + require_root + + local username="$1" + local instance="$2" + + [ -z "$username" ] && { log_error "Username required"; return 1; } + [ -z "$instance" ] && { log_error "Instance required"; return 1; } + + # Check if user exists + local existing=$(uci_get "user_$username") + [ -z "$existing" ] && { log_error "User '$username' not found"; return 1; } + + # Add instance to user's list + uci add_list hexojs.user_${username}.instances="$instance" + uci commit hexojs + + log_info "Granted '$username' access to '$instance'" +} + +cmd_user_revoke() { + require_root + + local username="$1" + local instance="$2" + + [ -z "$username" ] && { log_error "Username required"; return 1; } + [ -z "$instance" ] && { log_error "Instance required"; return 1; } + + # Remove instance from user's list + uci del_list hexojs.user_${username}.instances="$instance" 2>/dev/null + uci commit hexojs + + log_info "Revoked '$username' access from '$instance'" +} + +cmd_update_htpasswd() { + local htpasswd_dir=$(dirname "$HTPASSWD_FILE") + ensure_dir "$htpasswd_dir" + + > "$HTPASSWD_FILE" + + for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do + local username="${section#user_}" + local hash=$(uci_get "${section}.password_hash") + [ -n "$hash" ] && echo "${username}:${hash}" >> "$HTPASSWD_FILE" + done + + chmod 600 "$HTPASSWD_FILE" +} + +# =============================================== +# Authentication Commands +# =============================================== + +cmd_auth_enable() { + require_root + load_config + + local instance="${1:-default}" + load_instance_config "$instance" || { log_error "Instance not found"; return 1; } + + uci set hexojs.${instance}.auth_enabled='1' + uci commit hexojs + + log_info "Authentication enabled for '$instance'" + log_info "Configure HAProxy with: hexoctl auth haproxy $instance" +} + +cmd_auth_disable() { + require_root + load_config + + local instance="${1:-default}" + load_instance_config "$instance" || { log_error "Instance not found"; return 1; } + + uci set hexojs.${instance}.auth_enabled='0' + uci commit hexojs + + log_info "Authentication disabled for '$instance'" +} + +cmd_auth_status() { + load_config + + local instance="${1:-}" + + if [ -n "$instance" ]; then + load_instance_config "$instance" || { log_error "Instance not found"; return 1; } + local auth_enabled=$(uci_get "${instance}.auth_enabled") + local auth_users=$(uci_get "${instance}.auth_users") + + echo "Instance: $instance" + echo "Auth Enabled: $([ "$auth_enabled" = "1" ] && echo "yes" || echo "no")" + echo "Allowed Users: ${auth_users:-all}" + else + echo "Authentication Status:" + echo "---------------------" + + for inst in $(get_enabled_instances); do + load_instance_config "$inst" || continue + local auth_enabled=$(uci_get "${inst}.auth_enabled") + printf " %-15s %s\n" "$inst" "$([ "$auth_enabled" = "1" ] && echo "[AUTH]" || echo "[PUBLIC]")" + done + + echo "" + echo "Users:" + cmd_user_list | tail -n +3 + fi +} + +cmd_auth_haproxy() { + require_root + load_config + + local instance="${1:-default}" + load_instance_config "$instance" || { log_error "Instance not found"; return 1; } + + local auth_enabled=$(uci_get "${instance}.auth_enabled") + [ "$auth_enabled" != "1" ] && { log_error "Auth not enabled for '$instance'"; return 1; } + + # Update htpasswd + cmd_update_htpasswd + + local domain=$(uci_get "${instance}.domain") + [ -z "$domain" ] && domain="${instance}.hexo.local" + + log_info "HAProxy configuration for authenticated instance '$instance':" + echo "" + echo "# Add to HAProxy frontend https-in:" + echo " acl host_${instance}_hexo hdr(host) -i ${domain}" + echo " http-request auth realm \"SecuBox HexoJS\" if host_${instance}_hexo !{ http_auth(hexojs_users) }" + echo " use_backend hexo_${instance} if host_${instance}_hexo" + echo "" + echo "# Add userlist section:" + echo "userlist hexojs_users" + + for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do + local username="${section#user_}" + local hash=$(uci_get "${section}.password_hash") + [ -n "$hash" ] && echo " user ${username} password ${hash}" + done + + echo "" + echo "# Add backend:" + echo "backend hexo_${instance}" + echo " mode http" + echo " server hexo 127.0.0.1:${instance_port} check" + echo "" + + log_info "Copy the above to your HAProxy config and reload" +} + +# Generate full HAProxy config for all authenticated instances +cmd_auth_haproxy_all() { + require_root + load_config + + echo "# HexoJS Authenticated Instances - HAProxy Config" + echo "# Generated: $(date)" + echo "" + + echo "userlist hexojs_users" + for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do + local username="${section#user_}" + local hash=$(uci_get "${section}.password_hash") + [ -n "$hash" ] && echo " user ${username} password ${hash}" + done + echo "" + + for inst in $(get_enabled_instances); do + load_instance_config "$inst" || continue + local auth_enabled=$(uci_get "${inst}.auth_enabled") + [ "$auth_enabled" != "1" ] && continue + + local domain=$(uci_get "${inst}.domain") + [ -z "$domain" ] && domain="${inst}.hexo.local" + + echo "# Instance: $inst (port $instance_port)" + echo "acl host_${inst}_hexo hdr(host) -i ${domain}" + echo "http-request auth realm \"SecuBox HexoJS\" if host_${inst}_hexo !{ http_auth(hexojs_users) }" + echo "use_backend hexo_${inst} if host_${inst}_hexo" + echo "" + echo "backend hexo_${inst}" + echo " mode http" + echo " server hexo 127.0.0.1:${instance_port} check" + echo "" + done +} + +# Apply auth configuration to HAProxy (auto-configure) +cmd_auth_apply() { + require_root + load_config + + local instance="${1:-}" + local domain="${2:-}" + + [ -z "$instance" ] && { log_error "Instance name required"; return 1; } + [ -z "$domain" ] && domain="${instance}.secubox.local" + + local auth_enabled=$(uci_get "${instance}.auth_enabled") + [ "$auth_enabled" != "1" ] && { + log_info "Enabling auth for '$instance'..." + uci set hexojs.${instance}.auth_enabled='1' + uci commit hexojs + } + + # Save domain + uci set hexojs.${instance}.domain="$domain" + uci commit hexojs + + # Update htpasswd + cmd_update_htpasswd + + # Write userlist file for HAProxy include + local userlist_file="/srv/haproxy/config/hexojs-users.cfg" + log_info "Writing userlist to $userlist_file..." + + cat > "$userlist_file" << 'USERLIST_HEAD' +# HexoJS Users - Auto-generated by hexoctl +# Include this in haproxy.cfg before frontends +USERLIST_HEAD + echo "userlist hexojs_users" >> "$userlist_file" + + for section in $(uci show hexojs 2>/dev/null | grep '=user$' | cut -d'.' -f2 | cut -d'=' -f1); do + local username="${section#user_}" + local hash=$(uci_get "${section}.password_hash") + [ -n "$hash" ] && echo " user ${username} password ${hash}" >> "$userlist_file" + done + + # Check if haproxyctl exists + if command -v haproxyctl >/dev/null 2>&1; then + # Add backend pointing to static files (served by uhttpd) + local static_url="/static/${instance}/" + local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + log_info "Configuring HAProxy vhost: $domain" + + # Remove existing vhost if present + haproxyctl vhost del "$domain" 2>/dev/null + + # Add vhost pointing to uhttpd + haproxyctl vhost add "$domain" uhttpd "$router_ip" 80 2>/dev/null || true + + # The auth rules need to be added to HAProxy config manually or via template + log_info "Vhost created: $domain -> uhttpd" + log_info "" + log_info "Add these lines to HAProxy frontend https-in:" + echo "" + echo " # HexoJS Auth for $instance" + echo " acl host_${instance}_hexo hdr(host) -i ${domain}" + echo " http-request auth realm \"SecuBox HexoJS\" if host_${instance}_hexo !{ http_auth(hexojs_users) }" + echo "" + log_info "Then reload HAProxy: haproxyctl reload" + else + log_warn "haproxyctl not found" + log_info "Manual HAProxy config required - see: hexoctl auth haproxy $instance" + fi + + log_info "Auth applied for '$instance' at $domain" +} + +# =============================================== +# KISS Static Upload (No Hexo Build Required) +# =============================================== + +# Create a static-only instance (no Hexo, just serves files) +cmd_static_create() { + require_root + load_config + + local name="$1" + [ -z "$name" ] && { log_error "Instance name required"; return 1; } + + # Validate name + echo "$name" | grep -qE '^[a-z][a-z0-9_]*$' || { + log_error "Invalid name. Use lowercase letters, numbers, underscore." + return 1 + } + + # Find next available port + local port=4000 + while uci show hexojs 2>/dev/null | grep -q "port='$port'"; do + port=$((port + 1)) + done + + # Create UCI config + uci set hexojs.${name}=instance + uci set hexojs.${name}.enabled='1' + uci set hexojs.${name}.port="$port" + uci set hexojs.${name}.title="$name" + uci set hexojs.${name}.type='static' + uci commit hexojs + + # Create directory structure + local static_dir="$data_path/static/$name" + ensure_dir "$static_dir" + + # Create default index.html + cat > "$static_dir/index.html" << 'HTML' + + + + + + SecuBox Static Site + + + +
+

SecuBox Static Site

+

Upload your HTML files to get started

+
+ hexoctl static upload <file.html> [instance] +
+
+ + +HTML + + log_info "Static instance '$name' created on port $port" + log_info "Directory: $static_dir" + log_info "Upload files: hexoctl static upload $name" +} + +# Upload file to static instance (KISS fast publish) +cmd_static_upload() { + require_root + load_config + + local file="$1" + local instance="${2:-default}" + + [ -z "$file" ] && { log_error "File path required"; return 1; } + [ -f "$file" ] || { log_error "File not found: $file"; return 1; } + + local static_dir="$data_path/static/$instance" + [ -d "$static_dir" ] || { + log_info "Creating static instance '$instance'..." + cmd_static_create "$instance" + } + + local filename=$(basename "$file") + cp "$file" "$static_dir/$filename" + + log_info "Uploaded: $filename → $static_dir/" + log_info "URL: http://$(uci -q get network.lan.ipaddr || echo localhost):$(uci_get ${instance}.port || echo 4000)/$filename" +} + +# List files in static instance +cmd_static_list() { + load_config + + local instance="${1:-}" + + if [ -n "$instance" ]; then + local static_dir="$data_path/static/$instance" + [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } + + echo "Files in '$instance':" + ls -lh "$static_dir" | tail -n +2 + else + echo "Static Instances:" + echo "-----------------" + for dir in "$data_path/static"/*; do + [ -d "$dir" ] || continue + local name=$(basename "$dir") + local count=$(find "$dir" -type f | wc -l) + local port=$(uci_get "${name}.port") + printf " %-15s %3d files port:%s\n" "$name" "$count" "${port:-?}" + done + fi +} + +# Start simple static server (no Hexo, just Python/BusyBox httpd) +cmd_static_serve() { + require_root + load_config + + local instance="${1:-default}" + local static_dir="$data_path/static/$instance" + + [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } + + local port=$(uci_get "${instance}.port") + [ -z "$port" ] && port=4000 + + log_info "Serving '$instance' on port $port..." + + # Try Python first, fallback to busybox httpd + if command -v python3 >/dev/null 2>&1; then + cd "$static_dir" && python3 -m http.server "$port" --bind 0.0.0.0 + elif command -v busybox >/dev/null 2>&1; then + busybox httpd -f -p "$port" -h "$static_dir" + else + log_error "No HTTP server available (need python3 or busybox)" + return 1 + fi +} + +# Delete static instance +cmd_static_delete() { + require_root + load_config + + local name="$1" + [ -z "$name" ] && { log_error "Instance name required"; return 1; } + + local static_dir="$data_path/static/$name" + [ -d "$static_dir" ] || { log_error "Static instance '$name' not found"; return 1; } + + # Remove directory + rm -rf "$static_dir" + + # Remove UCI config if exists + uci delete hexojs.${name} 2>/dev/null + uci commit hexojs 2>/dev/null + + log_info "Static instance '$name' deleted" +} + +# Publish static instance to /www/ (immediate serving via uhttpd) +cmd_static_publish() { + require_root + load_config + + local instance="${1:-default}" + local static_dir="$data_path/static/$instance" + + [ -d "$static_dir" ] || { log_error "Static instance '$instance' not found"; return 1; } + + local publish_path="/www/static/${instance}" + ensure_dir "$publish_path" + + log_info "Publishing to $publish_path..." + rsync -av --delete "$static_dir/" "$publish_path/" + + local file_count=$(find "$publish_path" -type f | wc -l) + log_info "Published $file_count files" + log_info "URL: http://$(uci -q get network.lan.ipaddr || echo localhost)/static/${instance}/" +} + +# Quick upload + publish workflow (KISS one-liner) +cmd_static_quick() { + require_root + load_config + + local file="$1" + local instance="${2:-quick}" + + [ -z "$file" ] && { log_error "File path required"; return 1; } + [ -f "$file" ] || { log_error "File not found: $file"; return 1; } + + # Upload + cmd_static_upload "$file" "$instance" + + # Publish immediately + cmd_static_publish "$instance" +} + cmd_instance_list_json() { load_config @@ -1568,5 +2269,47 @@ case "${1:-}" in status-json) cmd_status_json ;; instance-list-json) cmd_instance_list_json ;; + user) + shift + case "${1:-}" in + add) shift; cmd_user_add "$@" ;; + del|delete) shift; cmd_user_del "$@" ;; + list) shift; cmd_user_list "$@" ;; + passwd) shift; cmd_user_passwd "$@" ;; + grant) shift; cmd_user_grant "$@" ;; + revoke) shift; cmd_user_revoke "$@" ;; + sync-secubox|sync) shift; cmd_user_sync_secubox "$@" ;; + *) echo "Usage: hexoctl user {add|del|list|passwd|grant|revoke|sync-secubox}" ;; + esac + ;; + + auth) + shift + case "${1:-}" in + enable) shift; cmd_auth_enable "$@" ;; + disable) shift; cmd_auth_disable "$@" ;; + status) shift; cmd_auth_status "$@" ;; + haproxy) shift; cmd_auth_haproxy "$@" ;; + haproxy-all) cmd_auth_haproxy_all ;; + apply) shift; cmd_auth_apply "$@" ;; + *) echo "Usage: hexoctl auth {enable|disable|status|haproxy|apply} [instance]" ;; + esac + ;; + + # KISS Static Upload (no Hexo, direct HTML) + static) + shift + case "${1:-}" in + create) shift; cmd_static_create "$@" ;; + upload) shift; cmd_static_upload "$@" ;; + publish) shift; cmd_static_publish "$@" ;; + quick) shift; cmd_static_quick "$@" ;; + list) shift; cmd_static_list "$@" ;; + serve) shift; cmd_static_serve "$@" ;; + delete) shift; cmd_static_delete "$@" ;; + *) echo "Usage: hexoctl static {create|upload|publish|quick|list|serve|delete} [instance]" ;; + esac + ;; + *) usage ;; esac diff --git a/package/secubox/secubox-app-saas-relay/Makefile b/package/secubox/secubox-app-saas-relay/Makefile new file mode 100644 index 00000000..75611a43 --- /dev/null +++ b/package/secubox/secubox-app-saas-relay/Makefile @@ -0,0 +1,37 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-saas-relay +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-saas-relay + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=SaaS Relay - Shared Session Proxy + DEPENDS:=+secubox-app-mitmproxy +secubox-app-haproxy + PKGARCH:=all +endef + +define Package/secubox-app-saas-relay/description + SecuBox SaaS Relay provides shared browser session proxy for team access + to external services like Claude.ai, ChatGPT, Google, GitHub etc. + Uses mitmproxy for cookie injection/capture with SecuBox authentication. +endef + +define Build/Compile +endef + +define Package/secubox-app-saas-relay/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/saas-relay $(1)/etc/config/saas-relay + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/saasctl $(1)/usr/sbin/saasctl +endef + +$(eval $(call BuildPackage,secubox-app-saas-relay)) diff --git a/package/secubox/secubox-app-saas-relay/files/etc/config/saas-relay b/package/secubox/secubox-app-saas-relay/files/etc/config/saas-relay new file mode 100644 index 00000000..d83dc016 --- /dev/null +++ b/package/secubox/secubox-app-saas-relay/files/etc/config/saas-relay @@ -0,0 +1,137 @@ +# SecuBox SaaS Relay Configuration +# Shared browser session proxy for team access to external services + +config main 'main' + option enabled '0' + option data_path '/srv/saas-relay' + option proxy_port '8890' + option intercept_mode 'inject' + # inject = inject stored cookies, capture = capture new cookies, both = full relay + +# Service definitions - external SaaS to relay +config service 'claude_ai' + option enabled '1' + option name 'Claude AI' + option emoji '🤖' + option domain 'claude.ai' + option cookie_domains 'claude.ai,.claude.ai' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +config service 'chatgpt' + option enabled '1' + option name 'ChatGPT' + option emoji '💬' + option domain 'chat.openai.com' + option cookie_domains 'openai.com,.openai.com,chat.openai.com' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +config service 'google' + option enabled '1' + option name 'Google' + option emoji '🔍' + option domain 'google.com' + option cookie_domains 'google.com,.google.com,accounts.google.com' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +config service 'github' + option enabled '1' + option name 'GitHub' + option emoji '🐙' + option domain 'github.com' + option cookie_domains 'github.com,.github.com' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +config service 'notion' + option enabled '0' + option name 'Notion' + option emoji '📝' + option domain 'notion.so' + option cookie_domains 'notion.so,.notion.so' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +config service 'slack' + option enabled '0' + option name 'Slack' + option emoji '💼' + option domain 'slack.com' + option cookie_domains 'slack.com,.slack.com' + option auth_required '1' + option status 'disconnected' + option last_check '0' + +# Access control - which SecuBox users can use which services +config access 'default_access' + option user '*' + list services 'claude_ai' + list services 'chatgpt' + list services 'google' + list services 'github' + +# Logging configuration +config logging 'logging' + option enabled '1' + option emoji_mode '1' + option log_requests '1' + option log_auth '1' + option max_entries '1000' + +# CDN Cache configuration +config cache 'cache' + option enabled '1' + option storage_path '/srv/saas-relay/cache' + option max_size_mb '500' + option default_ttl '3600' + option profile 'gandalf' + +# Cache profiles - configurable caching strategies +config cache_profile 'minimal' + option ttl '300' + option max_file_size_kb '100' + list content_types 'text/css' + list content_types 'application/javascript' + list content_types 'image/svg+xml' + +config cache_profile 'gandalf' + option ttl '3600' + option max_file_size_kb '5000' + list content_types 'text/css' + list content_types 'application/javascript' + list content_types 'image/png' + list content_types 'image/jpeg' + list content_types 'image/gif' + list content_types 'image/webp' + list content_types 'image/svg+xml' + list content_types 'font/woff' + list content_types 'font/woff2' + list content_types 'application/font-woff' + list content_types 'application/font-woff2' + +config cache_profile 'aggressive' + option ttl '86400' + option max_file_size_kb '20000' + list content_types '*' + list exclude_patterns '/api/' + list exclude_patterns '/auth/' + list exclude_patterns '/login' + list exclude_patterns '/logout' + +# Session replay - share sessions across SecuBox users +config session_replay 'session_replay' + option enabled '1' + option default_mode 'shared' + # shared = all users share the same session + # per_user = each SecuBox user gets their own session copy + # master = one user authenticates, others replay + option master_user 'admin' + option session_storage '/srv/saas-relay/sessions' + option sync_interval '60' diff --git a/package/secubox/secubox-app-saas-relay/files/usr/sbin/saasctl b/package/secubox/secubox-app-saas-relay/files/usr/sbin/saasctl new file mode 100755 index 00000000..72324dba --- /dev/null +++ b/package/secubox/secubox-app-saas-relay/files/usr/sbin/saasctl @@ -0,0 +1,1513 @@ +#!/bin/sh +# SecuBox SaaS Relay Controller +# Shared browser session proxy with cookie injection +# Uses dedicated mitmproxy-saas LXC container + +# Don't use set -e as it causes issues with config_foreach +# set -e + +CONFIG="saas-relay" +DATA_PATH="/srv/saas-relay" +COOKIES_PATH="$DATA_PATH/cookies" +LOG_PATH="$DATA_PATH/logs" +MITMPROXY_ADDON="$DATA_PATH/saas_relay_addon.py" + +# LXC Container settings +LXC_NAME="mitmproxy-saas" +LXC_PATH="/srv/lxc" +LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" +PROXY_PORT="8890" +WEB_PORT="8891" + +# Emoji status indicators +E_OK="✅" +E_WARN="⚠️" +E_ERR="❌" +E_WAIT="⏳" +E_LOCK="🔐" +E_UNLOCK="🔓" +E_COOKIE="🍪" +E_USER="👤" +E_LOG="📋" +E_RELAY="🔄" +E_CONNECT="🔗" +E_DISCONNECT="🔌" +E_CONTAINER="📦" + +. /lib/functions.sh + +log_info() { logger -t saas-relay -p daemon.info "$E_RELAY $*"; echo "$E_OK $*"; } +log_warn() { logger -t saas-relay -p daemon.warn "$E_WARN $*"; echo "$E_WARN $*"; } +log_error() { logger -t saas-relay -p daemon.err "$E_ERR $*"; echo "$E_ERR $*" >&2; } +log_auth() { logger -t saas-relay -p auth.info "$E_LOCK $*"; } + +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } + +ensure_dir() { + [ -d "$1" ] || mkdir -p "$1" +} + +require_root() { + [ "$(id -u)" = "0" ] || { log_error "Root required"; exit 1; } +} + +has_lxc() { + command -v lxc-start >/dev/null 2>&1 +} + +container_running() { + lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" +} + +container_exists() { + [ -d "$LXC_PATH/$LXC_NAME" ] +} + +# =========================================== +# Container Management +# =========================================== + +cmd_install() { + require_root + + if ! has_lxc; then + log_error "LXC not available. Install lxc packages first." + return 1 + fi + + if container_exists; then + log_warn "$E_CONTAINER Container $LXC_NAME already exists" + return 0 + fi + + log_info "$E_CONTAINER Installing SaaS Relay container..." + + # Create directories + ensure_dir "$DATA_PATH" + ensure_dir "$COOKIES_PATH" + ensure_dir "$LOG_PATH" + ensure_dir "$LXC_PATH/$LXC_NAME" + + # Create rootfs from mitmproxy Docker image + _create_container_rootfs || { log_error "Failed to create rootfs"; return 1; } + + # Create container config + _create_container_config || { log_error "Failed to create config"; return 1; } + + # Create mitmproxy addon + _generate_mitmproxy_addon + + # Create activity log + touch "$LOG_PATH/activity.log" + chmod 600 "$LOG_PATH/activity.log" + + uci_set "main.enabled" "1" + log_info "$E_OK SaaS Relay container installed" + log_info " Start with: saasctl start" + log_info " Web interface: http://:$WEB_PORT" + log_info " Proxy port: $PROXY_PORT" +} + +_create_container_rootfs() { + local rootfs="$LXC_ROOTFS" + local image="mitmproxy/mitmproxy" + local tag="latest" + local registry="registry-1.docker.io" + local arch + + # Detect architecture + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + armv7l) arch="arm" ;; + *) arch="amd64" ;; + esac + + log_info "$E_WAIT Extracting mitmproxy Docker image ($arch)..." + ensure_dir "$rootfs" + + # Get Docker Hub token + local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" 2>/dev/null | jsonfilter -e '@.token') + [ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; } + + # Get manifest list + local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ + --header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ + "https://$registry/v2/$image/manifests/$tag" 2>/dev/null) + + # Find digest for our architecture + local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest") + [ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; } + + # Get image manifest + local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ + --header="Accept: application/vnd.docker.distribution.manifest.v2+json" \ + "https://$registry/v2/$image/manifests/$digest" 2>/dev/null) + + # Extract all layers + local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest') + for layer in $layers; do + log_info " Downloading layer ${layer:7:12}..." + wget -q -O - --header="Authorization: Bearer $token" \ + "https://$registry/v2/$image/blobs/$layer" 2>/dev/null | tar -xzf - -C "$rootfs" 2>/dev/null || true + done + + # Verify mitmproxy is installed + [ -x "$rootfs/usr/bin/mitmproxy" ] || [ -x "$rootfs/usr/local/bin/mitmweb" ] || { + log_error "mitmproxy not found in extracted image" + return 1 + } + + log_info "$E_OK Container rootfs created" +} + +_create_container_config() { + cat > "$LXC_PATH/$LXC_NAME/config" << EOF +# SaaS Relay mitmproxy container +lxc.uts.name = $LXC_NAME + +# Rootfs +lxc.rootfs.path = dir:$LXC_ROOTFS + +# Host networking (shares host network namespace) +lxc.net.0.type = none + +# Mounts - minimal, no cgroup auto-mount (causes issues on some systems) +lxc.mount.auto = proc:rw sys:rw +lxc.mount.entry = /dev dev none bind,create=dir 0 0 +lxc.mount.entry = /srv/saas-relay srv/saas-relay none bind,create=dir 0 0 +lxc.mount.entry = /etc/resolv.conf etc/resolv.conf none bind,create=file 0 0 + +# Capabilities +lxc.cap.drop = sys_admin + +# Console +lxc.console.path = none +lxc.tty.max = 0 + +# Logging +lxc.log.file = /var/log/lxc/$LXC_NAME.log +lxc.log.level = WARN + +# Environment +lxc.environment = PATH=/usr/local/bin:/usr/bin:/bin +lxc.environment = HOME=/root +EOF + + # Create mount point in rootfs + ensure_dir "$LXC_ROOTFS/srv/saas-relay" + ensure_dir "/var/log/lxc" + log_info "$E_OK Container config created" +} + +cmd_uninstall() { + require_root + + if container_running; then + log_info "$E_DISCONNECT Stopping container..." + lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true + fi + + if container_exists; then + log_info "$E_CONTAINER Removing container..." + rm -rf "$LXC_PATH/$LXC_NAME" + fi + + uci_set "main.enabled" "0" + uci_set "main.status" "stopped" + log_info "$E_OK SaaS Relay container removed" +} + +# =========================================== +# Setup & Installation +# =========================================== + +cmd_setup() { + require_root + log_info "Setting up SaaS Relay..." + + ensure_dir "$DATA_PATH" + ensure_dir "$COOKIES_PATH" + ensure_dir "$LOG_PATH" + + # Create mitmproxy addon for cookie injection + _generate_mitmproxy_addon + + # Create activity log + touch "$LOG_PATH/activity.log" + chmod 600 "$LOG_PATH/activity.log" + + log_info "Setup complete: $DATA_PATH" +} + +_generate_services_json() { + # Generate services.json from UCI config for the container + local json_file="$DATA_PATH/services.json" + local entries="" + + config_load "$CONFIG" + + _export_service() { + local section="$1" + local enabled domain + + config_get enabled "$section" enabled "0" + [ "$enabled" = "1" ] || return + + config_get domain "$section" domain + [ -n "$domain" ] || return + + # Append entry with comma separator + if [ -n "$entries" ]; then + entries="${entries}," + fi + entries="${entries} + \"$domain\": \"$section\"" + } + + config_foreach _export_service service + + # Write final JSON + cat > "$json_file" << EOF +{${entries} +} +EOF + + log_info "$E_COOKIE Generated services.json" +} + +_generate_config_json() { + # Generate config.json from UCI config for cache and session settings + local json_file="$DATA_PATH/config.json" + + config_load "$CONFIG" + + # Cache settings + local cache_enabled cache_profile cache_max_size + config_get cache_enabled cache enabled "1" + config_get cache_profile cache profile "gandalf" + config_get cache_max_size cache max_size_mb "500" + + # Session replay settings + local session_enabled session_mode master_user + config_get session_enabled session_replay enabled "1" + config_get session_mode session_replay default_mode "shared" + config_get master_user session_replay master_user "admin" + + # Write config JSON + cat > "$json_file" << EOF +{ + "cache": { + "enabled": $([ "$cache_enabled" = "1" ] && echo "true" || echo "false"), + "profile": "$cache_profile", + "max_size_mb": $cache_max_size, + "storage_path": "$DATA_PATH/cache" + }, + "session_replay": { + "enabled": $([ "$session_enabled" = "1" ] && echo "true" || echo "false"), + "mode": "$session_mode", + "master_user": "$master_user" + } +} +EOF + + log_info "⚙️ Generated config.json" +} + +_generate_mitmproxy_addon() { + cat > "$MITMPROXY_ADDON" << 'PYTHON' +""" +SecuBox SaaS Relay - MITMProxy Addon +Cookie injection, CDN caching, and session replay for shared team access +""" + +import json +import os +import time +import hashlib +import re +from pathlib import Path +from mitmproxy import http, ctx +from typing import Optional, Dict, Any + +DATA_PATH = "/srv/saas-relay" +COOKIES_PATH = f"{DATA_PATH}/cookies" +CACHE_PATH = f"{DATA_PATH}/cache" +SESSIONS_PATH = f"{DATA_PATH}/sessions" +LOG_PATH = f"{DATA_PATH}/logs/activity.log" +SERVICES_FILE = f"{DATA_PATH}/services.json" +CONFIG_FILE = f"{DATA_PATH}/config.json" + +# Service domain mappings (loaded from JSON file) +SERVICES: Dict[str, str] = {} +CONFIG: Dict[str, Any] = {} + +# Cache profiles +CACHE_PROFILES = { + "minimal": { + "ttl": 300, + "max_file_size_kb": 100, + "content_types": ["text/css", "application/javascript", "image/svg+xml"], + "exclude_patterns": [] + }, + "gandalf": { + "ttl": 3600, + "max_file_size_kb": 5000, + "content_types": [ + "text/css", "application/javascript", + "image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml", + "font/woff", "font/woff2", "application/font-woff", "application/font-woff2" + ], + "exclude_patterns": [] + }, + "aggressive": { + "ttl": 86400, + "max_file_size_kb": 20000, + "content_types": ["*"], + "exclude_patterns": ["/api/", "/auth/", "/login", "/logout", "/session"] + } +} + +def load_config(): + """Load addon configuration from JSON file""" + global CONFIG + try: + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE) as f: + CONFIG = json.load(f) + ctx.log.info(f"⚙️ Loaded config from {CONFIG_FILE}") + else: + # Default config + CONFIG = { + "cache": {"enabled": True, "profile": "gandalf", "max_size_mb": 500}, + "session_replay": {"enabled": True, "mode": "shared", "master_user": "admin"} + } + except Exception as e: + ctx.log.error(f"Failed to load config: {e}") + CONFIG = {} + +def load_services(): + """Load service configurations from JSON file""" + global SERVICES + try: + if os.path.exists(SERVICES_FILE): + with open(SERVICES_FILE) as f: + SERVICES = json.load(f) + ctx.log.info(f"🔄 Loaded {len(SERVICES)} services from {SERVICES_FILE}") + else: + ctx.log.warn(f"Services file not found: {SERVICES_FILE}") + except Exception as e: + ctx.log.error(f"Failed to load services: {e}") + +def log_activity(emoji: str, user: str, service: str, action: str, details: str = ""): + """Log activity with emoji""" + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + entry = f"{timestamp} {emoji} [{user}] {service}: {action}" + if details: + entry += f" - {details}" + + try: + with open(LOG_PATH, "a") as f: + f.write(entry + "\n") + except: + pass + +def get_cookies_file(service: str, user: str = None) -> Path: + """Get cookie file path for service, optionally per-user""" + session_mode = CONFIG.get("session_replay", {}).get("mode", "shared") + + if session_mode == "per_user" and user: + return Path(SESSIONS_PATH) / user / f"{service}.json" + else: + return Path(COOKIES_PATH) / f"{service}.json" + +def load_cookies(service: str, user: str = None) -> dict: + """Load stored cookies for service""" + cookie_file = get_cookies_file(service, user) + if cookie_file.exists(): + try: + with open(cookie_file) as f: + return json.load(f) + except: + return {} + + # Fallback to master session in replay mode + session_cfg = CONFIG.get("session_replay", {}) + if session_cfg.get("mode") == "master" and user != session_cfg.get("master_user"): + master_file = get_cookies_file(service, session_cfg.get("master_user")) + if master_file.exists(): + try: + with open(master_file) as f: + return json.load(f) + except: + pass + + return {} + +def save_cookies(service: str, cookies: dict, user: str = None): + """Save cookies for service""" + cookie_file = get_cookies_file(service, user) + cookie_file.parent.mkdir(parents=True, exist_ok=True) + try: + with open(cookie_file, "w") as f: + json.dump(cookies, f, indent=2) + except Exception as e: + ctx.log.error(f"Failed to save cookies: {e}") + +# =========================================== +# CDN Cache Management +# =========================================== + +def get_cache_key(url: str, method: str = "GET") -> str: + """Generate cache key from URL""" + return hashlib.sha256(f"{method}:{url}".encode()).hexdigest()[:32] + +def get_cache_file(cache_key: str) -> Path: + """Get cache file path""" + # Distribute across subdirs for performance + subdir = cache_key[:2] + return Path(CACHE_PATH) / subdir / f"{cache_key}.cache" + +def get_cache_meta_file(cache_key: str) -> Path: + """Get cache metadata file path""" + subdir = cache_key[:2] + return Path(CACHE_PATH) / subdir / f"{cache_key}.meta" + +def is_cacheable(flow: http.HTTPFlow) -> bool: + """Check if response is cacheable based on profile""" + cache_cfg = CONFIG.get("cache", {}) + if not cache_cfg.get("enabled", True): + return False + + profile_name = cache_cfg.get("profile", "gandalf") + profile = CACHE_PROFILES.get(profile_name, CACHE_PROFILES["gandalf"]) + + # Check method + if flow.request.method not in ["GET", "HEAD"]: + return False + + # Check response code + if flow.response.status_code not in [200, 301, 302, 304]: + return False + + # Check URL exclusions + url = flow.request.pretty_url + for pattern in profile.get("exclude_patterns", []): + if pattern in url: + return False + + # Check content type + content_type = flow.response.headers.get("Content-Type", "") + allowed_types = profile.get("content_types", []) + + if "*" not in allowed_types: + type_match = any(ct in content_type for ct in allowed_types) + if not type_match: + return False + + # Check size + content_length = flow.response.headers.get("Content-Length", "0") + try: + size_kb = int(content_length) / 1024 + if size_kb > profile.get("max_file_size_kb", 5000): + return False + except: + pass + + # Check Cache-Control + cache_control = flow.response.headers.get("Cache-Control", "") + if "no-store" in cache_control or "private" in cache_control: + return False + + return True + +def get_cached_response(url: str) -> Optional[tuple]: + """Get cached response if valid, returns (content, headers, status)""" + cache_key = get_cache_key(url) + cache_file = get_cache_file(cache_key) + meta_file = get_cache_meta_file(cache_key) + + if not cache_file.exists() or not meta_file.exists(): + return None + + try: + with open(meta_file) as f: + meta = json.load(f) + + # Check expiry + if time.time() > meta.get("expires", 0): + cache_file.unlink(missing_ok=True) + meta_file.unlink(missing_ok=True) + return None + + with open(cache_file, "rb") as f: + content = f.read() + + return (content, meta.get("headers", {}), meta.get("status", 200)) + except: + return None + +def cache_response(flow: http.HTTPFlow): + """Store response in cache""" + cache_cfg = CONFIG.get("cache", {}) + profile_name = cache_cfg.get("profile", "gandalf") + profile = CACHE_PROFILES.get(profile_name, CACHE_PROFILES["gandalf"]) + + url = flow.request.pretty_url + cache_key = get_cache_key(url) + cache_file = get_cache_file(cache_key) + meta_file = get_cache_meta_file(cache_key) + + # Ensure directory exists + cache_file.parent.mkdir(parents=True, exist_ok=True) + + try: + # Store content + with open(cache_file, "wb") as f: + f.write(flow.response.content or b"") + + # Store metadata + ttl = profile.get("ttl", 3600) + + # Respect Cache-Control max-age if present + cache_control = flow.response.headers.get("Cache-Control", "") + max_age_match = re.search(r"max-age=(\d+)", cache_control) + if max_age_match: + ttl = min(ttl, int(max_age_match.group(1))) + + meta = { + "url": url, + "status": flow.response.status_code, + "headers": dict(flow.response.headers), + "cached_at": time.time(), + "expires": time.time() + ttl, + "ttl": ttl, + "size": len(flow.response.content or b"") + } + + with open(meta_file, "w") as f: + json.dump(meta, f, indent=2) + + ctx.log.info(f"📦 Cached: {url[:60]}... (TTL: {ttl}s)") + except Exception as e: + ctx.log.error(f"Cache write failed: {e}") + +def get_cache_stats() -> dict: + """Get cache statistics""" + cache_dir = Path(CACHE_PATH) + if not cache_dir.exists(): + return {"files": 0, "size_mb": 0} + + total_size = 0 + file_count = 0 + + for f in cache_dir.rglob("*.cache"): + total_size += f.stat().st_size + file_count += 1 + + return { + "files": file_count, + "size_mb": round(total_size / (1024 * 1024), 2) + } + +# =========================================== +# Main Addon Class +# =========================================== + +class SaaSRelay: + def __init__(self): + load_config() + load_services() + + # Ensure directories exist + Path(CACHE_PATH).mkdir(parents=True, exist_ok=True) + Path(SESSIONS_PATH).mkdir(parents=True, exist_ok=True) + Path(COOKIES_PATH).mkdir(parents=True, exist_ok=True) + + stats = get_cache_stats() + ctx.log.info(f"🔄 SaaS Relay addon loaded") + ctx.log.info(f"📦 Cache: {stats['files']} files, {stats['size_mb']} MB") + ctx.log.info(f"🔐 Session mode: {CONFIG.get('session_replay', {}).get('mode', 'shared')}") + + def request(self, flow: http.HTTPFlow): + """Handle incoming requests - check cache, inject cookies""" + host = flow.request.host + url = flow.request.pretty_url + + # Check cache first (before any network request) + cache_cfg = CONFIG.get("cache", {}) + if cache_cfg.get("enabled", True) and flow.request.method == "GET": + cached = get_cached_response(url) + if cached: + content, headers, status = cached + flow.response = http.Response.make( + status, + content, + headers + ) + flow.response.headers["X-SaaSRelay-Cache"] = "HIT" + ctx.log.info(f"⚡ Cache HIT: {url[:60]}...") + return + + # Find matching service for cookie injection + service = None + for domain, svc in SERVICES.items(): + if host.endswith(domain) or host == domain: + service = svc + break + + if not service: + return + + # Get authenticated user from header (set by HAProxy) + user = flow.request.headers.get("X-Auth-User", "anonymous") + + # Load and inject cookies (respecting session mode) + cookies = load_cookies(service, user) + if cookies: + existing = flow.request.cookies + for name, value in cookies.items(): + if name not in existing: + flow.request.cookies[name] = value + + log_activity("🍪", user, service, "inject", f"{len(cookies)} cookies") + ctx.log.info(f"🍪 Injected {len(cookies)} cookies for {service}") + + def response(self, flow: http.HTTPFlow): + """Handle responses - cache content, capture cookies""" + host = flow.request.host + url = flow.request.pretty_url + + # Skip if response came from cache + if flow.response.headers.get("X-SaaSRelay-Cache") == "HIT": + return + + # Cache response if eligible + if is_cacheable(flow): + cache_response(flow) + flow.response.headers["X-SaaSRelay-Cache"] = "MISS" + + # Find matching service for cookie capture + service = None + for domain, svc in SERVICES.items(): + if host.endswith(domain) or host == domain: + service = svc + break + + if not service: + return + + # Get authenticated user + user = flow.request.headers.get("X-Auth-User", "anonymous") + + # Capture Set-Cookie headers + set_cookies = flow.response.headers.get_all("Set-Cookie") + if set_cookies: + # In master mode, only master user can update cookies + session_cfg = CONFIG.get("session_replay", {}) + if session_cfg.get("mode") == "master": + if user != session_cfg.get("master_user", "admin"): + ctx.log.info(f"🔒 Session replay: {user} using master's session") + return + + cookies = load_cookies(service, user) + new_count = 0 + + for cookie_header in set_cookies: + # Parse cookie name=value + if '=' in cookie_header: + parts = cookie_header.split(';')[0] + name, _, value = parts.partition('=') + name = name.strip() + value = value.strip() + + if name and value and not value.startswith('deleted'): + cookies[name] = value + new_count += 1 + + if new_count > 0: + save_cookies(service, cookies, user) + log_activity("📥", user, service, "capture", f"{new_count} new cookies") + ctx.log.info(f"📥 Captured {new_count} cookies for {service}") + +addons = [SaaSRelay()] +PYTHON + + chmod 644 "$MITMPROXY_ADDON" + log_info "$E_COOKIE MITMProxy addon created (with caching & session replay)" +} + +# =========================================== +# Service Management +# =========================================== + +cmd_service_list() { + echo "$E_RELAY SaaS Services" + echo "==================" + echo "" + + config_load "$CONFIG" + + _print_service() { + local section="$1" + local enabled name emoji domain status + + config_get enabled "$section" enabled "0" + config_get name "$section" name "$section" + config_get emoji "$section" emoji "🔗" + config_get domain "$section" domain + config_get status "$section" status "unknown" + + [ -n "$domain" ] || return + + local status_emoji + case "$status" in + connected) status_emoji="$E_OK" ;; + disconnected) status_emoji="$E_DISCONNECT" ;; + error) status_emoji="$E_ERR" ;; + *) status_emoji="$E_WAIT" ;; + esac + + local enabled_mark + [ "$enabled" = "1" ] && enabled_mark="$E_UNLOCK" || enabled_mark="$E_LOCK" + + # Check cookie count + local cookie_file="$COOKIES_PATH/${section}.json" + local cookie_count=0 + [ -f "$cookie_file" ] && cookie_count=$(grep -c '"' "$cookie_file" 2>/dev/null | awk '{print int($1/2)}') + + printf " %s %s %-15s %-25s %s %s cookies\n" \ + "$enabled_mark" "$emoji" "$name" "$domain" "$status_emoji" "$cookie_count" + } + + config_foreach _print_service service +} + +cmd_service_enable() { + local service="$1" + [ -z "$service" ] && { echo "Usage: saasctl service enable "; return 1; } + + uci_set "${service}.enabled" "1" + log_info "$E_UNLOCK Enabled service: $service" +} + +cmd_service_disable() { + local service="$1" + [ -z "$service" ] && { echo "Usage: saasctl service disable "; return 1; } + + uci_set "${service}.enabled" "0" + log_info "$E_LOCK Disabled service: $service" +} + +cmd_service_add() { + local id="$1" + local name="$2" + local domain="$3" + local emoji="${4:-🔗}" + + [ -z "$id" ] || [ -z "$name" ] || [ -z "$domain" ] && { + echo "Usage: saasctl service add [emoji]" + return 1 + } + + uci set ${CONFIG}.${id}=service + uci set ${CONFIG}.${id}.enabled='1' + uci set ${CONFIG}.${id}.name="$name" + uci set ${CONFIG}.${id}.emoji="$emoji" + uci set ${CONFIG}.${id}.domain="$domain" + uci set ${CONFIG}.${id}.cookie_domains="$domain,.$domain" + uci set ${CONFIG}.${id}.auth_required='1' + uci set ${CONFIG}.${id}.status='disconnected' + uci commit ${CONFIG} + + log_info "$emoji Added service: $name ($domain)" +} + +# =========================================== +# Cookie Management +# =========================================== + +cmd_cookie_list() { + local service="$1" + + echo "$E_COOKIE Cookie Store" + echo "==============" + echo "" + + if [ -n "$service" ]; then + local cookie_file="$COOKIES_PATH/${service}.json" + if [ -f "$cookie_file" ]; then + echo "Service: $service" + cat "$cookie_file" | head -50 + else + echo "No cookies for $service" + fi + else + for f in "$COOKIES_PATH"/*.json; do + [ -f "$f" ] || continue + local svc=$(basename "$f" .json) + local count=$(grep -c '"' "$f" 2>/dev/null | awk '{print int($1/2)}') + local size=$(stat -c%s "$f" 2>/dev/null || echo 0) + printf " %-20s %3d cookies %s bytes\n" "$svc" "$count" "$size" + done + fi +} + +cmd_cookie_import() { + local service="$1" + local file="$2" + + [ -z "$service" ] || [ -z "$file" ] && { + echo "Usage: saasctl cookie import " + echo " saasctl cookie import - (read from stdin)" + return 1 + } + + ensure_dir "$COOKIES_PATH" + + if [ "$file" = "-" ]; then + cat > "$COOKIES_PATH/${service}.json" + else + cp "$file" "$COOKIES_PATH/${service}.json" + fi + + chmod 600 "$COOKIES_PATH/${service}.json" + log_info "$E_COOKIE Imported cookies for $service" +} + +cmd_cookie_export() { + local service="$1" + [ -z "$service" ] && { echo "Usage: saasctl cookie export "; return 1; } + + local cookie_file="$COOKIES_PATH/${service}.json" + [ -f "$cookie_file" ] || { log_error "No cookies for $service"; return 1; } + + cat "$cookie_file" +} + +cmd_cookie_clear() { + local service="$1" + + if [ -n "$service" ]; then + rm -f "$COOKIES_PATH/${service}.json" + log_info "$E_COOKIE Cleared cookies for $service" + else + rm -f "$COOKIES_PATH"/*.json + log_info "$E_COOKIE Cleared all cookies" + fi +} + +# =========================================== +# Cache Management +# =========================================== + +E_CACHE="📦" + +cmd_cache_status() { + echo "$E_CACHE CDN Cache Status" + echo "===================" + echo "" + + local enabled=$(uci_get cache.enabled) + local profile=$(uci_get cache.profile) + local max_size=$(uci_get cache.max_size_mb) + + echo "Enabled: $([ "$enabled" = "1" ] && echo "$E_OK Yes" || echo "$E_ERR No")" + echo "Profile: $profile" + echo "Max Size: ${max_size:-500} MB" + echo "" + + # Calculate cache stats + local cache_dir="$DATA_PATH/cache" + if [ -d "$cache_dir" ]; then + local file_count=$(find "$cache_dir" -name "*.cache" 2>/dev/null | wc -l) + local total_size=$(du -sh "$cache_dir" 2>/dev/null | awk '{print $1}') + echo "Files: $file_count" + echo "Size: $total_size" + + # Show oldest and newest cache entries + local oldest=$(find "$cache_dir" -name "*.meta" -exec stat -c '%Y %n' {} \; 2>/dev/null | sort -n | head -1) + local newest=$(find "$cache_dir" -name "*.meta" -exec stat -c '%Y %n' {} \; 2>/dev/null | sort -rn | head -1) + + if [ -n "$oldest" ]; then + local oldest_time=$(echo "$oldest" | awk '{print $1}') + local oldest_age=$(( ($(date +%s) - oldest_time) / 3600 )) + echo "Oldest: ${oldest_age}h ago" + fi + else + echo "Cache directory not initialized" + fi +} + +cmd_cache_clear() { + require_root + local cache_dir="$DATA_PATH/cache" + + if [ -d "$cache_dir" ]; then + local file_count=$(find "$cache_dir" -name "*.cache" 2>/dev/null | wc -l) + rm -rf "$cache_dir"/* + log_info "$E_CACHE Cleared cache ($file_count files)" + else + log_warn "Cache directory not found" + fi +} + +cmd_cache_profile() { + local profile="$1" + + if [ -z "$profile" ]; then + echo "$E_CACHE Available Cache Profiles" + echo "===========================" + echo "" + echo " minimal - Small files only (CSS, JS, SVG), 5min TTL" + echo " gandalf - Standard caching (CSS, JS, images, fonts), 1h TTL" + echo " aggressive - Cache everything except API/auth paths, 24h TTL" + echo "" + echo "Current: $(uci_get cache.profile)" + return + fi + + case "$profile" in + minimal|gandalf|aggressive) + uci_set "cache.profile" "$profile" + log_info "$E_CACHE Cache profile set to: $profile" + log_info " Restart relay to apply: saasctl restart" + ;; + *) + log_error "Unknown profile: $profile" + log_error "Available: minimal, gandalf, aggressive" + return 1 + ;; + esac +} + +cmd_cache_enable() { + uci_set "cache.enabled" "1" + log_info "$E_CACHE CDN caching enabled" +} + +cmd_cache_disable() { + uci_set "cache.enabled" "0" + log_info "$E_CACHE CDN caching disabled" +} + +# =========================================== +# Session Replay Management +# =========================================== + +E_SESSION="🎭" + +cmd_session_status() { + echo "$E_SESSION Session Replay Status" + echo "========================" + echo "" + + local enabled=$(uci_get session_replay.enabled) + local mode=$(uci_get session_replay.default_mode) + local master=$(uci_get session_replay.master_user) + + echo "Enabled: $([ "$enabled" = "1" ] && echo "$E_OK Yes" || echo "$E_ERR No")" + echo "Mode: $mode" + [ "$mode" = "master" ] && echo "Master: $master" + echo "" + + # Show per-user sessions if in per_user mode + local sessions_dir="$DATA_PATH/sessions" + if [ -d "$sessions_dir" ]; then + echo "User Sessions:" + for user_dir in "$sessions_dir"/*/; do + [ -d "$user_dir" ] || continue + local user=$(basename "$user_dir") + local session_count=$(ls "$user_dir"/*.json 2>/dev/null | wc -l) + echo " $E_USER $user: $session_count services" + done + fi +} + +cmd_session_mode() { + local mode="$1" + + if [ -z "$mode" ]; then + echo "$E_SESSION Session Replay Modes" + echo "=======================" + echo "" + echo " shared - All users share the same session cookies" + echo " per_user - Each user gets their own session copy" + echo " master - One user authenticates, others replay their session" + echo "" + echo "Current: $(uci_get session_replay.default_mode)" + return + fi + + case "$mode" in + shared|per_user|master) + uci_set "session_replay.default_mode" "$mode" + log_info "$E_SESSION Session mode set to: $mode" + log_info " Restart relay to apply: saasctl restart" + ;; + *) + log_error "Unknown mode: $mode" + log_error "Available: shared, per_user, master" + return 1 + ;; + esac +} + +cmd_session_master() { + local user="$1" + [ -z "$user" ] && { echo "Usage: saasctl session master "; return 1; } + + uci_set "session_replay.master_user" "$user" + log_info "$E_SESSION Master user set to: $user" +} + +cmd_session_enable() { + uci_set "session_replay.enabled" "1" + log_info "$E_SESSION Session replay enabled" +} + +cmd_session_disable() { + uci_set "session_replay.enabled" "0" + log_info "$E_SESSION Session replay disabled" +} + +# =========================================== +# Relay Control +# =========================================== + +cmd_start() { + require_root + + local enabled=$(uci_get main.enabled) + [ "$enabled" = "1" ] || { log_warn "SaaS Relay is disabled"; return 1; } + + if ! container_exists; then + log_error "$E_CONTAINER Container not installed. Run: saasctl install" + return 1 + fi + + if container_running; then + log_warn "$E_CONTAINER Container already running" + return 0 + fi + + # Ensure addon exists + [ -f "$MITMPROXY_ADDON" ] || _generate_mitmproxy_addon + + # Generate config files from UCI + _generate_services_json + _generate_config_json + + # Ensure cache directory exists + ensure_dir "$DATA_PATH/cache" + ensure_dir "$DATA_PATH/sessions" + + log_info "$E_RELAY Starting SaaS Relay container..." + + # Create startup script in container + cat > "$LXC_ROOTFS/start-saas-relay.sh" << 'STARTSCRIPT' +#!/bin/sh +cd /srv/saas-relay + +# Wait for network +sleep 2 + +# Start mitmweb +# HAProxy handles user auth, mitmproxy uses fixed password "saas" +# Using regular mode (not transparent) +ADDON_ARG="" +[ -f /srv/saas-relay/saas_relay_addon.py ] && ADDON_ARG="-s /srv/saas-relay/saas_relay_addon.py" + +exec /usr/local/bin/mitmweb \ + --mode regular \ + --listen-host 0.0.0.0 \ + --listen-port 8890 \ + --web-host 0.0.0.0 \ + --web-port 8891 \ + --no-web-open-browser \ + --set confdir=/srv/saas-relay/.mitmproxy \ + --set block_global=false \ + --set web_password="saas" \ + $ADDON_ARG \ + 2>&1 | tee /srv/saas-relay/logs/mitmproxy.log +STARTSCRIPT + chmod +x "$LXC_ROOTFS/start-saas-relay.sh" + + # Ensure mitmproxy config dir exists + ensure_dir "$DATA_PATH/.mitmproxy" + + # Start container + lxc-start -n "$LXC_NAME" -d -- /start-saas-relay.sh + + # Wait for startup + sleep 3 + + if container_running; then + uci_set "main.status" "running" + log_info "$E_OK SaaS Relay started" + log_info " Web interface: http://$(uci -q get network.lan.ipaddr || echo 192.168.255.1):$WEB_PORT" + log_info " Proxy port: $PROXY_PORT" + else + log_error "Container failed to start. Check logs: tail /var/log/lxc/$LXC_NAME.log" + return 1 + fi +} + +cmd_stop() { + require_root + log_info "$E_DISCONNECT Stopping SaaS Relay..." + + if container_running; then + lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true + fi + + uci_set "main.status" "stopped" + log_info "$E_OK SaaS Relay stopped" +} + +cmd_restart() { + cmd_stop + sleep 2 + cmd_start +} + +cmd_status() { + echo "$E_RELAY SaaS Relay Status" + echo "=====================" + echo "" + + local enabled=$(uci_get main.enabled) + local status=$(uci_get main.status) + + echo "Enabled: $([ "$enabled" = "1" ] && echo "$E_OK Yes" || echo "$E_ERR No")" + + # Check actual container status + if container_running; then + echo "Container: $E_OK Running ($LXC_NAME)" + uci_set "main.status" "running" 2>/dev/null + elif container_exists; then + echo "Container: $E_WARN Stopped ($LXC_NAME)" + uci_set "main.status" "stopped" 2>/dev/null + else + echo "Container: $E_ERR Not installed" + fi + + echo "Proxy Port: $PROXY_PORT" + echo "Web Port: $WEB_PORT" + echo "Data Path: $DATA_PATH" + echo "" + + # Check if ports are listening + if container_running; then + echo "$E_CONNECT Port Status:" + if netstat -tln 2>/dev/null | grep -q ":$PROXY_PORT "; then + echo " Proxy ($PROXY_PORT): $E_OK Listening" + else + echo " Proxy ($PROXY_PORT): $E_WARN Not listening" + fi + if netstat -tln 2>/dev/null | grep -q ":$WEB_PORT "; then + echo " Web ($WEB_PORT): $E_OK Listening" + else + echo " Web ($WEB_PORT): $E_WARN Not listening" + fi + echo "" + fi + + # CDN Cache summary + echo "$E_CACHE CDN Cache:" + local cache_enabled=$(uci_get cache.enabled) + local cache_profile=$(uci_get cache.profile) + echo " Enabled: $([ "$cache_enabled" = "1" ] && echo "Yes" || echo "No") | Profile: ${cache_profile:-gandalf}" + if [ -d "$DATA_PATH/cache" ]; then + local cache_files=$(find "$DATA_PATH/cache" -name "*.cache" 2>/dev/null | wc -l) + local cache_size=$(du -sh "$DATA_PATH/cache" 2>/dev/null | awk '{print $1}') + echo " Files: $cache_files | Size: ${cache_size:-0}" + fi + echo "" + + # Session Replay summary + echo "$E_SESSION Session Replay:" + local session_enabled=$(uci_get session_replay.enabled) + local session_mode=$(uci_get session_replay.default_mode) + local master_user=$(uci_get session_replay.master_user) + echo " Enabled: $([ "$session_enabled" = "1" ] && echo "Yes" || echo "No") | Mode: ${session_mode:-shared}" + [ "$session_mode" = "master" ] && echo " Master User: $master_user" + echo "" + + # Cookie summary + echo "$E_COOKIE Cookie Summary:" + local total_cookies=0 + for f in "$COOKIES_PATH"/*.json; do + [ -f "$f" ] || continue + local count=$(grep -c '"' "$f" 2>/dev/null | awk '{print int($1/2)}') + total_cookies=$((total_cookies + count)) + done + echo " Total cookies stored: $total_cookies" + echo "" + + # Recent activity + echo "$E_LOG Recent Activity:" + if [ -f "$LOG_PATH/activity.log" ]; then + tail -5 "$LOG_PATH/activity.log" | sed 's/^/ /' + else + echo " No activity logged" + fi +} + +cmd_logs() { + local lines="${1:-50}" + + if [ -f "$LOG_PATH/mitmproxy.log" ]; then + tail -n "$lines" "$LOG_PATH/mitmproxy.log" + else + echo "No mitmproxy logs available" + fi +} + +cmd_shell() { + if ! container_running; then + log_error "$E_CONTAINER Container not running" + return 1 + fi + + lxc-attach -n "$LXC_NAME" -- /bin/sh +} + +# =========================================== +# Activity Log +# =========================================== + +cmd_log() { + local lines="${1:-20}" + + echo "$E_LOG Activity Log (last $lines)" + echo "==========================" + echo "" + + if [ -f "$LOG_PATH/activity.log" ]; then + tail -n "$lines" "$LOG_PATH/activity.log" + else + echo "No activity logged" + fi +} + +cmd_log_clear() { + require_root + > "$LOG_PATH/activity.log" + log_info "$E_LOG Activity log cleared" +} + +# =========================================== +# HAProxy Integration +# =========================================== + +cmd_configure_haproxy() { + require_root + log_info "$E_CONNECT Configuring HAProxy backend..." + + # SaaS relay uses the web interface port for HAProxy access + # Users access the mitmproxy web UI which shows flows and allows configuration + local web_port="$WEB_PORT" + local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + # Create backend for SaaS relay + if command -v haproxyctl >/dev/null 2>&1; then + # Remove old backend if exists + uci -q delete haproxy.backend_saas_relay 2>/dev/null || true + uci -q delete haproxy.saas_relay_server 2>/dev/null || true + + # Create new backend + uci set haproxy.backend_saas_relay=backend + uci set haproxy.backend_saas_relay.name='saas_relay' + uci set haproxy.backend_saas_relay.mode='http' + uci set haproxy.backend_saas_relay.balance='roundrobin' + uci set haproxy.backend_saas_relay.enabled='1' + + # Add server + uci set haproxy.saas_relay_server=server + uci set haproxy.saas_relay_server.backend='saas_relay' + uci set haproxy.saas_relay_server.name='mitmproxy-saas' + uci set haproxy.saas_relay_server.address="$lan_ip" + uci set haproxy.saas_relay_server.port="$web_port" + uci set haproxy.saas_relay_server.weight='100' + uci set haproxy.saas_relay_server.check='1' + uci set haproxy.saas_relay_server.enabled='1' + + uci commit haproxy + + log_info "$E_OK HAProxy backend configured: saas_relay -> $lan_ip:$web_port" + else + log_warn "haproxyctl not found" + fi +} + +cmd_emancipate() { + local domain="$1" + [ -z "$domain" ] && { echo "Usage: saasctl emancipate "; return 1; } + + require_root + log_info "$E_CONNECT Emancipating SaaS Relay at $domain..." + + # Create HAProxy vhost with auth + if command -v haproxyctl >/dev/null 2>&1; then + haproxyctl vhost add "$domain" saas_relay + haproxyctl auth enable "$domain" + haproxyctl cert add "$domain" + + log_info "$E_OK SaaS Relay exposed at https://$domain (auth required)" + else + log_error "haproxyctl not found" + return 1 + fi +} + +# =========================================== +# Usage +# =========================================== + +usage() { + cat << EOF +$E_RELAY SecuBox SaaS Relay - Shared Session Proxy with CDN Cache + +Usage: saasctl [options] + +Container: + install Install mitmproxy-saas container + uninstall Remove container and data + shell Open shell in container + +Setup: + setup Initialize data directories + configure-haproxy Setup HAProxy backend + emancipate Expose relay with SSL + auth + +Control: + start Start relay container + stop Stop relay container + restart Restart relay container + status Show relay status + logs [lines] Show mitmproxy logs + +Services: + service list List configured services + service enable Enable a service + service disable Disable a service + service add [emoji] + +Cookies: + cookie list [service] List stored cookies + cookie import Import cookies + cookie export Export cookies as JSON + cookie clear [service] Clear cookies + +CDN Cache: + cache status Show cache statistics + cache clear Clear all cached content + cache profile [name] Set cache profile (minimal/gandalf/aggressive) + cache enable Enable CDN caching + cache disable Disable CDN caching + +Session Replay: + session status Show session replay status + session mode [mode] Set mode (shared/per_user/master) + session master Set master user for replay mode + session enable Enable session replay + session disable Disable session replay + +Logging: + log [lines] Show activity log + log clear Clear activity log + +Cache Profiles: + minimal - CSS, JS, SVG only - 5min TTL - safe for most sites + gandalf - CSS, JS, images, fonts - 1h TTL - balanced caching + aggressive - Everything except /api/ /auth/ - 24h TTL - max savings + +Examples: + saasctl install + saasctl cache profile gandalf + saasctl session mode master + saasctl session master admin + saasctl service add anthropic "Anthropic" "console.anthropic.com" "🧠" + saasctl cookie import claude_ai cookies.json + saasctl emancipate relay.secubox.in + saasctl start + +EOF +} + +# =========================================== +# Main +# =========================================== + +case "${1:-}" in + install) shift; cmd_install "$@" ;; + uninstall) shift; cmd_uninstall "$@" ;; + setup) shift; cmd_setup "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + restart) shift; cmd_restart "$@" ;; + status) shift; cmd_status "$@" ;; + logs) shift; cmd_logs "$@" ;; + shell) shift; cmd_shell "$@" ;; + + service) + shift + case "${1:-}" in + list) shift; cmd_service_list "$@" ;; + enable) shift; cmd_service_enable "$@" ;; + disable) shift; cmd_service_disable "$@" ;; + add) shift; cmd_service_add "$@" ;; + *) echo "Usage: saasctl service {list|enable|disable|add}" ;; + esac + ;; + + cookie) + shift + case "${1:-}" in + list) shift; cmd_cookie_list "$@" ;; + import) shift; cmd_cookie_import "$@" ;; + export) shift; cmd_cookie_export "$@" ;; + clear) shift; cmd_cookie_clear "$@" ;; + *) echo "Usage: saasctl cookie {list|import|export|clear}" ;; + esac + ;; + + cache) + shift + case "${1:-}" in + status) shift; cmd_cache_status "$@" ;; + clear) shift; cmd_cache_clear "$@" ;; + profile) shift; cmd_cache_profile "$@" ;; + enable) shift; cmd_cache_enable "$@" ;; + disable) shift; cmd_cache_disable "$@" ;; + *) echo "Usage: saasctl cache {status|clear|profile|enable|disable}" ;; + esac + ;; + + session) + shift + case "${1:-}" in + status) shift; cmd_session_status "$@" ;; + mode) shift; cmd_session_mode "$@" ;; + master) shift; cmd_session_master "$@" ;; + enable) shift; cmd_session_enable "$@" ;; + disable) shift; cmd_session_disable "$@" ;; + *) echo "Usage: saasctl session {status|mode|master|enable|disable}" ;; + esac + ;; + + log) + shift + case "${1:-}" in + clear) shift; cmd_log_clear "$@" ;; + *) cmd_log "$@" ;; + esac + ;; + + configure-haproxy) shift; cmd_configure_haproxy "$@" ;; + emancipate) shift; cmd_emancipate "$@" ;; + + *) usage ;; +esac diff --git a/package/secubox/secubox-core-users/files/usr/sbin/secubox-users b/package/secubox/secubox-core-users/files/usr/sbin/secubox-users index 21f4a425..7ea10e11 100644 --- a/package/secubox/secubox-core-users/files/usr/sbin/secubox-users +++ b/package/secubox/secubox-core-users/files/usr/sbin/secubox-users @@ -18,8 +18,9 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_title() { echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${BLUE}$1${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } -# Load library +# Load libraries . /usr/lib/secubox/users-lib.sh 2>/dev/null || true +. /usr/lib/secubox/users-login-stats.sh 2>/dev/null || true # UCI helpers uci_get() { uci -q get ${CONFIG}.$1; } @@ -310,22 +311,103 @@ cmd_list() { log_title "SecuBox Users" local users=$(uci show ${CONFIG} 2>/dev/null | grep "=user$" | cut -d'.' -f2 | cut -d'=' -f1) + local domain=$(get_domain) if [ -z "$users" ]; then echo "No users configured." return fi - printf "%-15s %-25s %-8s %s\n" "USERNAME" "EMAIL" "ENABLED" "SERVICES" - echo "─────────────────────────────────────────────────────────────────────────" + printf "%-12s %-22s %-18s %6s %6s\n" "USERNAME" "EMAIL" "LAST LOGIN" "OK" "FAIL" + echo "────────────────────────────────────────────────────────────────────────────" for user in $users; do local email=$(uci_get ${user}.email) - local enabled=$(uci_get ${user}.enabled) - local services=$(uci -q get ${CONFIG}.${user}.services 2>/dev/null | tr ' ' ',') - [ "$enabled" = "1" ] && enabled="yes" || enabled="no" - printf "%-15s %-25s %-8s %s\n" "$user" "$email" "$enabled" "$services" + + # Get login stats if library loaded + local last_login="n/a" + local ok_count="0" + local fail_count="0" + + if type get_user_stats_summary >/dev/null 2>&1; then + local stats=$(get_user_stats_summary "$user" "$domain" 2>/dev/null) + last_login=$(echo "$stats" | cut -d'|' -f1) + ok_count=$(echo "$stats" | cut -d'|' -f2) + fail_count=$(echo "$stats" | cut -d'|' -f3) + [ -z "$last_login" ] && last_login="never" + fi + + printf "%-12s %-22s %-18s %6s %6s\n" "$user" "${email:-$user@$domain}" "${last_login:0:18}" "${ok_count:-0}" "${fail_count:-0}" done + + echo "" + echo "Use 'secubox-users stats ' for detailed login statistics" +} + +# Show login stats for a user +cmd_stats() { + local username="$1" + + if [ -z "$username" ]; then + log_error "Usage: secubox-users stats " + return 1 + fi + + # Check user exists + if ! uci -q get ${CONFIG}.${username} >/dev/null 2>&1; then + log_error "User not found: $username" + return 1 + fi + + local domain=$(get_domain) + log_title "Login Statistics: $username" + + echo "" + printf "%-12s %-24s %8s %8s\n" "SERVICE" "LAST LOGIN" "SUCCESS" "FAILURE" + echo "────────────────────────────────────────────────────────────────" + + # Nextcloud + local nc_stats=$(get_nextcloud_stats "$username" 2>/dev/null || echo "||") + local nc_last=$(echo "$nc_stats" | cut -d'|' -f1) + local nc_ok=$(echo "$nc_stats" | cut -d'|' -f2) + local nc_fail=$(echo "$nc_stats" | cut -d'|' -f3) + printf "%-12s %-24s %8s %8s\n" "Nextcloud" "${nc_last:-never}" "${nc_ok:-0}" "${nc_fail:-0}" + + # PeerTube + local pt_stats=$(get_peertube_stats "$username" 2>/dev/null || echo "||") + local pt_last=$(echo "$pt_stats" | cut -d'|' -f1) + local pt_ok=$(echo "$pt_stats" | cut -d'|' -f2) + local pt_fail=$(echo "$pt_stats" | cut -d'|' -f3) + printf "%-12s %-24s %8s %8s\n" "PeerTube" "${pt_last:-never}" "${pt_ok:-0}" "${pt_fail:-0}" + + # Jabber + local jb_stats=$(get_jabber_stats "$username" "$domain" 2>/dev/null || echo "||") + local jb_last=$(echo "$jb_stats" | cut -d'|' -f1) + local jb_ok=$(echo "$jb_stats" | cut -d'|' -f2) + local jb_fail=$(echo "$jb_stats" | cut -d'|' -f3) + printf "%-12s %-24s %8s %8s\n" "Jabber" "${jb_last:-never}" "${jb_ok:-0}" "${jb_fail:-0}" + + # Matrix + local mx_stats=$(get_matrix_stats "$username" 2>/dev/null || echo "||") + local mx_last=$(echo "$mx_stats" | cut -d'|' -f1) + local mx_ok=$(echo "$mx_stats" | cut -d'|' -f2) + local mx_fail=$(echo "$mx_stats" | cut -d'|' -f3) + printf "%-12s %-24s %8s %8s\n" "Matrix" "${mx_last:-never}" "${mx_ok:-0}" "${mx_fail:-0}" + + # Email + local em_stats=$(get_email_stats "$username" "$domain" 2>/dev/null || echo "||") + local em_last=$(echo "$em_stats" | cut -d'|' -f1) + local em_ok=$(echo "$em_stats" | cut -d'|' -f2) + local em_fail=$(echo "$em_stats" | cut -d'|' -f3) + printf "%-12s %-24s %8s %8s\n" "Email" "${em_last:-never}" "${em_ok:-0}" "${em_fail:-0}" + + echo "────────────────────────────────────────────────────────────────" + + # Totals + local total_ok=$((${nc_ok:-0} + ${pt_ok:-0} + ${jb_ok:-0} + ${mx_ok:-0} + ${em_ok:-0})) + local total_fail=$((${nc_fail:-0} + ${pt_fail:-0} + ${jb_fail:-0} + ${mx_fail:-0} + ${em_fail:-0})) + printf "%-12s %-24s %8s %8s\n" "TOTAL" "" "$total_ok" "$total_fail" + echo "" } # Sync user to services (re-create if missing) @@ -382,7 +464,8 @@ Commands: add [pass] [services] Create user on all/specified services del Delete user from all services passwd [new_pass] Update password on all services - list List all SecuBox users + list List all users with login stats + stats Show detailed login statistics sync [user] Sync user(s) to services status Show status and available services @@ -407,6 +490,7 @@ case "$1" in list|ls) cmd_list ;; sync) shift; cmd_sync "$@" ;; status) cmd_status ;; + stats) shift; cmd_stats "$@" ;; help|--help|-h|'') show_help ;; *) log_error "Unknown command: $1"; show_help; exit 1 ;; esac