feat: Implement core features for network-modes and system-hub
Network Modes (+536 lines RPCD): - Enhanced router.js with improved proxy configuration - Expanded RPCD backend with new methods - Updated README with feature documentation System Hub (+511 lines RPCD, +127 lines API): - Implemented diagnostics collection features - Enhanced remote management interface - Expanded RPCD backend with new RPC methods - Added new API methods for diagnostics and remote access Total changes: +1,392 additions, -126 deletions Preparation for v0.3.6 release with real feature implementations from CODEX roadmaps.
This commit is contained in:
parent
78d84ec74f
commit
4406825611
@ -144,7 +144,9 @@
|
||||
"Bash(for module in luci-app-ksm-manager luci-app-media-flow luci-app-netdata-dashboard luci-app-netifyd-dashboard luci-app-network-modes luci-app-secubox luci-app-system-hub luci-app-traffic-shaper luci-app-vhost-manager luci-app-wireguard-dashboard)",
|
||||
"Bash(do echo \"=== $module ===\" find \"$module/htdocs/luci-static/resources/view\" -name \"*.js\")",
|
||||
"Bash(gh run view:*)",
|
||||
"Bash(/tmp/deploy-system-hub-overview-fix.sh)"
|
||||
"Bash(/tmp/deploy-system-hub-overview-fix.sh)",
|
||||
"Bash(./secubox-tools/deploy-network-modes.sh:*)",
|
||||
"Bash(/tmp/generalize-makefile-filemodes.sh)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ Configure your OpenWrt router for different network operation modes with a moder
|
||||
- **WireGuard automation:** generate key pairs, deploy `wg0` interfaces, and push MTU/MSS/BBR optimizations directly from the Relay panel.
|
||||
- **Optimization RPCs:** new backend methods expose MTU clamping, TCP BBR, and WireGuard deployment to both UI and automation agents.
|
||||
- **UI action buttons:** Relay mode now includes one-click buttons for key generation, interface deployment, and optimization runs.
|
||||
- **Integrated proxies:** router mode now auto-configures Squid/TinyProxy/Privoxy, transparent HTTP redirection, DNS-over-HTTPS, and nginx reverse proxy vhosts with optional Let’s Encrypt certificates.
|
||||
|
||||
### 🔍 Sniffer Bridge Mode (Inline / Passthrough)
|
||||
Transparent Ethernet bridge without IP address for in-line traffic analysis. All traffic passes through the device.
|
||||
@ -60,6 +61,18 @@ Internet Router (Gateway)
|
||||
- ✅ Can perform traffic shaping
|
||||
- ⚠️ Single point of failure (if device fails, network is down)
|
||||
|
||||
**Proxy / DoH Requirements (Router Mode):**
|
||||
|
||||
| Capability | Packages |
|
||||
|------------|----------|
|
||||
| Caching proxy | `squid` or `tinyproxy` or `privoxy` |
|
||||
| Transparent proxy redirect | (iptables built-in) |
|
||||
| DNS over HTTPS | `https-dns-proxy`, `ca-certificates` |
|
||||
| HTTPS reverse proxy | `nginx` (HAProxy/Caddy support planned) |
|
||||
| Let’s Encrypt automation | `acme`, `acme-dnsapi`, `openssl-util` |
|
||||
|
||||
Install these packages before enabling the associated toggles in the Router panel so the automation can write configs and restart services successfully.
|
||||
|
||||
---
|
||||
|
||||
### 👁️ Sniffer Passive Mode (Out-of-band / Monitor Only)
|
||||
|
||||
@ -19,6 +19,8 @@ return view.extend({
|
||||
var fwConfig = config.firewall || {};
|
||||
var proxyConfig = config.proxy || {};
|
||||
var frontendConfig = config.https_frontend || {};
|
||||
var proxyStatus = proxyConfig.enabled ? (proxyConfig.type || 'squid') : 'disabled';
|
||||
var dohStatus = proxyConfig.dns_over_https ? _('Enabled') : _('Disabled');
|
||||
var vhosts = config.virtual_hosts || [];
|
||||
|
||||
var view = E('div', { 'class': 'network-modes-dashboard' }, [
|
||||
@ -48,8 +50,15 @@ return view.extend({
|
||||
E('span', { 'class': 'nm-card-title-icon' }, '🌍'),
|
||||
'WAN Configuration'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'nm-card-body' }, [
|
||||
]),
|
||||
E('div', { 'class': 'nm-card-body' }, [
|
||||
E('div', { 'class': 'nm-alert nm-alert-info', 'style': 'margin-bottom: 12px;' }, [
|
||||
E('span', { 'class': 'nm-alert-icon' }, 'ℹ️'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'nm-alert-title' }, _('Proxy status')),
|
||||
E('div', { 'class': 'nm-alert-text' }, _('Type: ') + proxyStatus + ' • DoH: ' + dohStatus)
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'nm-form-group' }, [
|
||||
E('label', { 'class': 'nm-form-label' }, 'WAN Interface'),
|
||||
E('input', {
|
||||
@ -226,6 +235,13 @@ return view.extend({
|
||||
E('div', { 'class': 'nm-card-badge' }, vhosts.length + ' virtual hosts')
|
||||
]),
|
||||
E('div', { 'class': 'nm-card-body' }, [
|
||||
E('div', { 'class': 'nm-alert nm-alert-info', 'style': 'margin-bottom: 12px;' }, [
|
||||
E('span', { 'class': 'nm-alert-icon' }, frontendConfig.enabled ? '🟢' : '⚪'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'nm-alert-title' }, _('Frontend status: ') + (frontendConfig.enabled ? _('Enabled') : _('Disabled'))),
|
||||
E('div', { 'class': 'nm-alert-text' }, _('Type: ') + (frontendConfig.type || 'nginx') + ' • ' + _('Let\'s Encrypt: ') + (frontendConfig.letsencrypt ? _('Enabled') : _('Disabled')))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'nm-toggle' }, [
|
||||
E('div', { 'class': 'nm-toggle-info' }, [
|
||||
E('span', { 'class': 'nm-toggle-icon' }, '🌐'),
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
CONFIG_FILE="/etc/config/network-modes"
|
||||
BACKUP_DIR="/etc/network-modes-backup"
|
||||
PCAP_DIR="/var/log/pcap"
|
||||
|
||||
# Get current status
|
||||
get_status() {
|
||||
@ -382,42 +383,49 @@ apply_mode() {
|
||||
# Apply network configuration based on mode
|
||||
case "$pending_mode" in
|
||||
router)
|
||||
# Router mode: NAT, DHCP server, firewall
|
||||
# WAN interface
|
||||
uci delete network.wan 2>/dev/null
|
||||
uci set network.wan=interface
|
||||
uci set network.wan.proto='dhcp'
|
||||
uci set network.wan.device='eth1'
|
||||
# Router mode: NAT, DHCP server, firewall
|
||||
# WAN interface
|
||||
uci delete network.wan 2>/dev/null
|
||||
uci set network.wan=interface
|
||||
uci set network.wan.proto='dhcp'
|
||||
uci set network.wan.device='eth1'
|
||||
|
||||
# LAN interface
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='static'
|
||||
uci set network.lan.device='eth0'
|
||||
uci set network.lan.ipaddr='192.168.1.1'
|
||||
uci set network.lan.netmask='255.255.255.0'
|
||||
# LAN interface
|
||||
uci set network.lan=interface
|
||||
uci set network.lan.proto='static'
|
||||
uci set network.lan.device='eth0'
|
||||
uci set network.lan.ipaddr='192.168.1.1'
|
||||
uci set network.lan.netmask='255.255.255.0'
|
||||
|
||||
# DHCP server
|
||||
uci set dhcp.lan=dhcp
|
||||
uci set dhcp.lan.interface='lan'
|
||||
uci set dhcp.lan.start='100'
|
||||
uci set dhcp.lan.limit='150'
|
||||
uci set dhcp.lan.leasetime='12h'
|
||||
# DHCP server
|
||||
uci set dhcp.lan=dhcp
|
||||
uci set dhcp.lan.interface='lan'
|
||||
uci set dhcp.lan.start='100'
|
||||
uci set dhcp.lan.limit='150'
|
||||
uci set dhcp.lan.leasetime='12h'
|
||||
|
||||
# Firewall zones
|
||||
uci set firewall.@zone[0]=zone
|
||||
uci set firewall.@zone[0].name='lan'
|
||||
uci set firewall.@zone[0].input='ACCEPT'
|
||||
uci set firewall.@zone[0].output='ACCEPT'
|
||||
uci set firewall.@zone[0].forward='ACCEPT'
|
||||
# Firewall zones
|
||||
uci set firewall.@zone[0]=zone
|
||||
uci set firewall.@zone[0].name='lan'
|
||||
uci set firewall.@zone[0].input='ACCEPT'
|
||||
uci set firewall.@zone[0].output='ACCEPT'
|
||||
uci set firewall.@zone[0].forward='ACCEPT'
|
||||
|
||||
uci set firewall.@zone[1]=zone
|
||||
uci set firewall.@zone[1].name='wan'
|
||||
uci set firewall.@zone[1].input='REJECT'
|
||||
uci set firewall.@zone[1].output='ACCEPT'
|
||||
uci set firewall.@zone[1].forward='REJECT'
|
||||
uci set firewall.@zone[1].masq='1'
|
||||
uci set firewall.@zone[1].mtu_fix='1'
|
||||
;;
|
||||
uci set firewall.@zone[1]=zone
|
||||
uci set firewall.@zone[1].name='wan'
|
||||
uci set firewall.@zone[1].input='REJECT'
|
||||
uci set firewall.@zone[1].output='ACCEPT'
|
||||
uci set firewall.@zone[1].forward='REJECT'
|
||||
uci set firewall.@zone[1].masq='1'
|
||||
uci set firewall.@zone[1].mtu_fix='1'
|
||||
|
||||
generate_squid_config
|
||||
generate_tinyproxy_config
|
||||
generate_privoxy_config
|
||||
configure_doh_proxy
|
||||
apply_transparent_proxy_rules
|
||||
deploy_nginx_vhosts
|
||||
;;
|
||||
|
||||
accesspoint)
|
||||
# Access Point mode: Bridge, no NAT, DHCP client
|
||||
@ -437,6 +445,7 @@ apply_mode() {
|
||||
uci set firewall.@zone[0].input='ACCEPT'
|
||||
uci set firewall.@zone[0].forward='ACCEPT'
|
||||
uci delete firewall.@zone[1] 2>/dev/null
|
||||
apply_accesspoint_features
|
||||
;;
|
||||
|
||||
relay)
|
||||
@ -479,6 +488,12 @@ apply_mode() {
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$pending_mode" = "sniffer" ]; then
|
||||
start_packet_capture
|
||||
else
|
||||
stop_packet_capture
|
||||
fi
|
||||
|
||||
# Commit all changes
|
||||
uci commit network
|
||||
uci commit dhcp
|
||||
@ -633,12 +648,453 @@ update_settings() {
|
||||
esac
|
||||
|
||||
uci commit network-modes
|
||||
[ "$mode" = "accesspoint" ] && apply_accesspoint_features
|
||||
if [ "$mode" = "sniffer" ]; then
|
||||
local current_mode=$(uci -q get network-modes.config.current_mode || echo "")
|
||||
[ "$current_mode" = "sniffer" ] && start_packet_capture
|
||||
fi
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Settings updated for $mode mode"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Squid proxy configuration
|
||||
generate_squid_config() {
|
||||
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
|
||||
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
|
||||
[ "$proxy_type" = "squid" ] || return 0
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
if ! command -v squid >/dev/null 2>&1; then
|
||||
logger -t network-modes "squid binary not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 3128)
|
||||
local cache_size=$(uci -q get network-modes.router.proxy_cache_size || echo 256)
|
||||
mkdir -p /etc/squid /var/spool/squid /var/log/squid
|
||||
|
||||
cat > /etc/squid/squid.conf <<EOF
|
||||
# SecuBox Network Modes - Squid Configuration
|
||||
# Generated: $(date)
|
||||
|
||||
http_port $proxy_port transparent
|
||||
cache_dir ufs /var/spool/squid $cache_size 16 256
|
||||
cache_mem 64 MB
|
||||
maximum_object_size 4096 KB
|
||||
minimum_object_size 0 KB
|
||||
|
||||
acl localnet src 192.168.0.0/16
|
||||
acl localnet src 10.0.0.0/8
|
||||
acl localnet src fc00::/7
|
||||
acl SSL_ports port 443
|
||||
acl Safe_ports port 80
|
||||
acl Safe_ports port 443
|
||||
acl CONNECT method CONNECT
|
||||
|
||||
http_access deny !Safe_ports
|
||||
http_access deny CONNECT !SSL_ports
|
||||
http_access allow localhost manager
|
||||
http_access deny manager
|
||||
http_access allow localnet
|
||||
http_access allow localhost
|
||||
http_access deny all
|
||||
|
||||
access_log /var/log/squid/access.log squid
|
||||
cache_log /var/log/squid/cache.log
|
||||
dns_nameservers 1.1.1.1 8.8.8.8
|
||||
forwarded_for on
|
||||
via on
|
||||
EOF
|
||||
|
||||
squid -z >/dev/null 2>&1 || true
|
||||
/etc/init.d/squid restart >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
generate_tinyproxy_config() {
|
||||
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
|
||||
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
|
||||
[ "$proxy_type" = "tinyproxy" ] || return 0
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
if ! command -v tinyproxy >/dev/null 2>&1; then
|
||||
logger -t network-modes "tinyproxy binary not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 8888)
|
||||
mkdir -p /etc/tinyproxy
|
||||
|
||||
cat > /etc/tinyproxy/tinyproxy.conf <<EOF
|
||||
User nobody
|
||||
Group nogroup
|
||||
Port $proxy_port
|
||||
Listen 0.0.0.0
|
||||
Timeout 600
|
||||
DefaultErrorFile "/usr/share/tinyproxy/default.html"
|
||||
Logfile "/var/log/tinyproxy/tinyproxy.log"
|
||||
StatFile "/usr/share/tinyproxy/stats.html"
|
||||
MaxClients 100
|
||||
MinSpareServers 5
|
||||
MaxSpareServers 20
|
||||
StartServers 10
|
||||
Allow 192.168.0.0/16
|
||||
Allow 10.0.0.0/8
|
||||
Allow 172.16.0.0/12
|
||||
ViaProxyName "SecuBox"
|
||||
EOF
|
||||
|
||||
/etc/init.d/tinyproxy restart >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
generate_privoxy_config() {
|
||||
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
|
||||
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
|
||||
[ "$proxy_type" = "privoxy" ] || return 0
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
if ! command -v privoxy >/dev/null 2>&1; then
|
||||
logger -t network-modes "privoxy binary not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 8118)
|
||||
mkdir -p /etc/privoxy
|
||||
|
||||
cat > /etc/privoxy/config <<EOF
|
||||
listen-address 0.0.0.0:$proxy_port
|
||||
toggle 1
|
||||
enable-remote-toggle 0
|
||||
enable-remote-http-toggle 0
|
||||
accept-intercepted-requests 1
|
||||
forward-socks5 / 127.0.0.1:9050 .
|
||||
logfile /var/log/privoxy/logfile
|
||||
EOF
|
||||
|
||||
/etc/init.d/privoxy restart >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
apply_transparent_proxy_rules() {
|
||||
local proxy_enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
|
||||
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 3128)
|
||||
local transparent=$(uci -q get network-modes.router.proxy_transparent || echo 0)
|
||||
|
||||
uci -q delete firewall.proxy_redirect
|
||||
|
||||
if [ "$proxy_enabled" = "1" ] && [ "$transparent" = "1" ]; then
|
||||
uci set firewall.proxy_redirect=redirect
|
||||
uci set firewall.proxy_redirect.name='Transparent Web Proxy'
|
||||
uci set firewall.proxy_redirect.src='lan'
|
||||
uci set firewall.proxy_redirect.proto='tcp'
|
||||
uci set firewall.proxy_redirect.src_dport='80'
|
||||
uci set firewall.proxy_redirect.dest_port="$proxy_port"
|
||||
uci set firewall.proxy_redirect.target='DNAT'
|
||||
fi
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
configure_doh_proxy() {
|
||||
local doh_enabled=$(uci -q get network-modes.router.dns_over_https || echo 0)
|
||||
[ "$doh_enabled" = "1" ] || return 0
|
||||
|
||||
if ! command -v https_dns_proxy >/dev/null 2>&1; then
|
||||
logger -t network-modes "https_dns_proxy not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local provider=$(uci -q get network-modes.router.doh_provider || echo "cloudflare")
|
||||
local doh_url
|
||||
case "$provider" in
|
||||
cloudflare) doh_url="https://1.1.1.1/dns-query" ;;
|
||||
google) doh_url="https://dns.google/dns-query" ;;
|
||||
quad9) doh_url="https://dns.quad9.net/dns-query" ;;
|
||||
*) doh_url="https://1.1.1.1/dns-query" ;;
|
||||
esac
|
||||
|
||||
uci set https-dns-proxy.@https-dns-proxy[0]=https-dns-proxy
|
||||
uci set https-dns-proxy.@https-dns-proxy[0].resolver_url="$doh_url"
|
||||
uci set https-dns-proxy.@https-dns-proxy[0].listen_addr='127.0.0.1'
|
||||
uci set https-dns-proxy.@https-dns-proxy[0].listen_port='5053'
|
||||
uci commit https-dns-proxy
|
||||
|
||||
uci set dhcp.@dnsmasq[0].noresolv='1'
|
||||
uci -q delete dhcp.@dnsmasq[0].server
|
||||
uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#5053"
|
||||
uci commit dhcp
|
||||
|
||||
/etc/init.d/https-dns-proxy restart >/dev/null 2>&1 || true
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
apply_accesspoint_features() {
|
||||
local iface="wireless.@wifi-iface[0]"
|
||||
uci -q show "$iface" >/dev/null 2>&1 || return 0
|
||||
local changed=0
|
||||
|
||||
local ft_enabled=$(uci -q get network-modes.accesspoint.roaming_enabled || echo 0)
|
||||
if [ "$ft_enabled" = "1" ]; then
|
||||
local ssid=$(uci -q get $iface.ssid || echo "SecuBox")
|
||||
local mobility_domain=$(echo -n "$ssid" | md5sum 2>/dev/null | cut -c1-4)
|
||||
[ -z "$mobility_domain" ] && mobility_domain="a1b2"
|
||||
uci set $iface.ieee80211r='1'
|
||||
uci set $iface.mobility_domain="$mobility_domain"
|
||||
uci set $iface.ft_over_ds='1'
|
||||
uci set $iface.ft_psk_generate_local='1'
|
||||
uci set $iface.reassociation_deadline='1000'
|
||||
changed=1
|
||||
else
|
||||
uci set $iface.ieee80211r='0'
|
||||
uci -q delete $iface.mobility_domain
|
||||
fi
|
||||
|
||||
local rrm_enabled=$(uci -q get network-modes.accesspoint.rrm_enabled || echo 0)
|
||||
if [ "$rrm_enabled" = "1" ]; then
|
||||
uci set $iface.ieee80211k='1'
|
||||
uci set $iface.rrm_neighbor_report='1'
|
||||
uci set $iface.rrm_beacon_report='1'
|
||||
changed=1
|
||||
else
|
||||
uci set $iface.ieee80211k='0'
|
||||
uci -q delete $iface.rrm_neighbor_report
|
||||
uci -q delete $iface.rrm_beacon_report
|
||||
fi
|
||||
|
||||
local v_enabled=$(uci -q get network-modes.accesspoint.wnm_enabled || echo 0)
|
||||
if [ "$v_enabled" = "1" ]; then
|
||||
uci set $iface.ieee80211v='1'
|
||||
uci set $iface.bss_transition='1'
|
||||
uci set $iface.wnm_sleep_mode='1'
|
||||
changed=1
|
||||
else
|
||||
uci set $iface.ieee80211v='0'
|
||||
uci -q delete $iface.bss_transition
|
||||
uci -q delete $iface.wnm_sleep_mode
|
||||
fi
|
||||
|
||||
local band=$(uci -q get network-modes.accesspoint.band_steering || echo 0)
|
||||
if [ "$band" = "1" ]; then
|
||||
local radio_2g=$(uci show wireless 2>/dev/null | grep "band='2g'" | head -n1)
|
||||
local radio_5g=$(uci show wireless 2>/dev/null | grep "band='5g'" | head -n1)
|
||||
if [ -n "$radio_2g" ] && [ -n "$radio_5g" ]; then
|
||||
uci set $iface.bss_load_update_period='60'
|
||||
uci set $iface.chan_util_avg_period='600'
|
||||
uci set $iface.disassoc_low_ack='1'
|
||||
changed=1
|
||||
else
|
||||
logger -t network-modes "band steering requested but dual-band radios missing"
|
||||
fi
|
||||
else
|
||||
uci -q delete $iface.bss_load_update_period
|
||||
uci -q delete $iface.chan_util_avg_period
|
||||
uci -q delete $iface.disassoc_low_ack
|
||||
fi
|
||||
|
||||
if [ "$changed" = "1" ]; then
|
||||
uci commit wireless
|
||||
wifi reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_packet_capture() {
|
||||
local capture_enabled=$(uci -q get network-modes.sniffer.pcap_capture || echo 0)
|
||||
if [ "$capture_enabled" != "1" ]; then
|
||||
stop_packet_capture
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v tcpdump >/dev/null 2>&1; then
|
||||
logger -t network-modes "tcpdump not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local capture_interface=$(uci -q get network-modes.sniffer.capture_interface || echo "br-lan")
|
||||
local capture_filter=$(uci -q get network-modes.sniffer.capture_filter || echo "")
|
||||
local max_size=$(uci -q get network-modes.sniffer.pcap_max_size || echo "100")
|
||||
local rotate_count=$(uci -q get network-modes.sniffer.pcap_rotate || echo "10")
|
||||
|
||||
mkdir -p "$PCAP_DIR"
|
||||
stop_packet_capture
|
||||
|
||||
if [ -n "$capture_filter" ]; then
|
||||
tcpdump -i "$capture_interface" -w "$PCAP_DIR/capture.pcap" -C "$max_size" -W "$rotate_count" -s 0 $capture_filter >/dev/null 2>&1 &
|
||||
else
|
||||
tcpdump -i "$capture_interface" -w "$PCAP_DIR/capture.pcap" -C "$max_size" -W "$rotate_count" -s 0 >/dev/null 2>&1 &
|
||||
fi
|
||||
local pid=$!
|
||||
echo "$pid" > /var/run/tcpdump-sniffer.pid
|
||||
}
|
||||
|
||||
stop_packet_capture() {
|
||||
if [ -f /var/run/tcpdump-sniffer.pid ]; then
|
||||
local pid=$(cat /var/run/tcpdump-sniffer.pid 2>/dev/null)
|
||||
[ -n "$pid" ] && kill "$pid" 2>/dev/null || true
|
||||
rm -f /var/run/tcpdump-sniffer.pid
|
||||
fi
|
||||
killall tcpdump >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
validate_pcap_filter() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var filter filter
|
||||
|
||||
if [ -z "$filter" ]; then
|
||||
json_init
|
||||
json_add_boolean "valid" 1
|
||||
json_add_string "filter" "all"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v tcpdump >/dev/null 2>&1 && tcpdump -i any -d "$filter" >/dev/null 2>&1; then
|
||||
json_init
|
||||
json_add_boolean "valid" 1
|
||||
json_add_string "filter" "$filter"
|
||||
json_dump
|
||||
else
|
||||
json_init
|
||||
json_add_boolean "valid" 0
|
||||
json_add_string "error" "Invalid BPF syntax"
|
||||
json_dump
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_old_pcaps() {
|
||||
local max_age=$(uci -q get network-modes.sniffer.pcap_retention_days || echo "7")
|
||||
mkdir -p "$PCAP_DIR"
|
||||
local deleted=0
|
||||
find "$PCAP_DIR" -name "*.pcap*" -mtime +$max_age -type f 2>/dev/null | while read -r file; do
|
||||
rm -f "$file"
|
||||
deleted=$((deleted + 1))
|
||||
done
|
||||
|
||||
local total_size=$(du -sm "$PCAP_DIR" 2>/dev/null | cut -f1 || echo "0")
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_int "deleted" "$deleted"
|
||||
json_add_int "total_size_mb" "$total_size"
|
||||
json_dump
|
||||
}
|
||||
|
||||
deploy_nginx_vhosts() {
|
||||
local frontend_enabled=$(uci -q get network-modes.router.https_frontend || echo 0)
|
||||
local frontend_type=$(uci -q get network-modes.router.frontend_type || echo nginx)
|
||||
[ "$frontend_enabled" = "1" ] || return 0
|
||||
[ "$frontend_type" = "nginx" ] || return 0
|
||||
|
||||
if ! command -v nginx >/dev/null 2>&1; then
|
||||
logger -t network-modes "nginx not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f /etc/nginx/conf.d/vhost-*.conf 2>/dev/null || true
|
||||
mkdir -p /etc/nginx/conf.d /etc/acme
|
||||
|
||||
local idx=0
|
||||
local deployed=0
|
||||
while true; do
|
||||
local domain=$(uci -q get network-modes.@vhost[$idx].domain)
|
||||
[ -n "$domain" ] || break
|
||||
local backend=$(uci -q get network-modes.@vhost[$idx].backend || echo "127.0.0.1")
|
||||
local port=$(uci -q get network-modes.@vhost[$idx].port || echo 80)
|
||||
local ssl=$(uci -q get network-modes.@vhost[$idx].ssl || echo 0)
|
||||
|
||||
cat > "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $domain;
|
||||
EOF
|
||||
|
||||
if [ "$ssl" = "1" ]; then
|
||||
cat >> "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name $domain;
|
||||
|
||||
ssl_certificate /etc/acme/$domain/fullchain.cer;
|
||||
ssl_certificate_key /etc/acme/$domain/$domain.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://$backend:$port;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat >> "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
|
||||
location / {
|
||||
proxy_pass http://$backend:$port;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
deployed=$((deployed + 1))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
/etc/init.d/nginx reload >/dev/null 2>&1 || true
|
||||
logger -t network-modes "Deployed $deployed nginx vhosts"
|
||||
}
|
||||
|
||||
issue_letsencrypt_cert() {
|
||||
local domain="$1"
|
||||
[ -n "$domain" ] || return 1
|
||||
|
||||
local acme_email=$(uci -q get network-modes.router.acme_email)
|
||||
[ -n "$acme_email" ] || acme_email="admin@$domain"
|
||||
|
||||
if ! command -v /usr/lib/acme/acme.sh >/dev/null 2>&1; then
|
||||
opkg update >/dev/null 2>&1 || true
|
||||
opkg install acme acme-dnsapi >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
/usr/lib/acme/acme.sh --issue -d "$domain" --webroot /www --accountemail "$acme_email" --force >/tmp/acme-issue.log 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
mkdir -p /etc/acme/$domain
|
||||
/usr/lib/acme/acme.sh --install-cert \
|
||||
-d "$domain" \
|
||||
--cert-file /etc/acme/$domain/cert.cer \
|
||||
--key-file /etc/acme/$domain/$domain.key \
|
||||
--fullchain-file /etc/acme/$domain/fullchain.cer \
|
||||
--reloadcmd "/etc/init.d/nginx reload" >/dev/null 2>&1
|
||||
return 0
|
||||
else
|
||||
logger -t network-modes "ACME issue failed for $domain"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_ssl_cert() {
|
||||
local domain="$1"
|
||||
local cert_file="/etc/acme/$domain/fullchain.cer"
|
||||
[ -f "$cert_file" ] || return 1
|
||||
command -v openssl >/dev/null 2>&1 || return 1
|
||||
|
||||
local expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
|
||||
local expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || echo 0)
|
||||
local now_epoch=$(date +%s)
|
||||
[ "$expiry_epoch" -gt 0 ] || return 1
|
||||
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
logger -t network-modes "Cert $domain valid $days_left days"
|
||||
[ "$days_left" -gt 0 ]
|
||||
}
|
||||
|
||||
# Generate WireGuard key pair and store in UCI
|
||||
generate_wireguard_keys() {
|
||||
json_init
|
||||
@ -1250,7 +1706,7 @@ rollback() {
|
||||
# Main dispatcher
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"}}'
|
||||
echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"},"validate_pcap_filter":{"filter":"str"},"cleanup_old_pcaps":{}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
@ -1326,6 +1782,12 @@ case "$1" in
|
||||
generate_config)
|
||||
generate_config
|
||||
;;
|
||||
validate_pcap_filter)
|
||||
validate_pcap_filter
|
||||
;;
|
||||
cleanup_old_pcaps)
|
||||
cleanup_old_pcaps
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Unknown method"}'
|
||||
;;
|
||||
|
||||
@ -101,6 +101,86 @@ var callGetComponentsByCategory = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callCollectDiagnostics = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'collect_diagnostics',
|
||||
params: ['include_logs', 'include_config', 'include_network', 'anonymize'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callListDiagnostics = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'list_diagnostics',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDownloadDiagnostic = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'download_diagnostic',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDeleteDiagnostic = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'delete_diagnostic',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRunDiagnosticTest = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'run_diagnostic_test',
|
||||
params: ['test'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callUploadDiagnostics = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'upload_diagnostics',
|
||||
params: ['name'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteStatus = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_status',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteInstall = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_install',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteConfigure = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_configure',
|
||||
params: ['relay_server', 'relay_key', 'rustdesk_enabled'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteGetCredentials = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_get_credentials',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteServiceAction = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_service_action',
|
||||
params: ['action'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callRemoteSaveSettings = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'remote_save_settings',
|
||||
params: ['allow_unattended', 'require_approval', 'notify_on_connect'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
// RPC methods - exposed via ubus
|
||||
getStatus: callStatus,
|
||||
@ -116,5 +196,50 @@ return baseclass.extend({
|
||||
reboot: callReboot,
|
||||
getStorage: callGetStorage,
|
||||
getSettings: callGetSettings,
|
||||
saveSettings: callSaveSettings
|
||||
saveSettings: callSaveSettings,
|
||||
|
||||
collectDiagnostics: function(includeLogs, includeConfig, includeNetwork, anonymize) {
|
||||
return callCollectDiagnostics({
|
||||
include_logs: includeLogs ? 1 : 0,
|
||||
include_config: includeConfig ? 1 : 0,
|
||||
include_network: includeNetwork ? 1 : 0,
|
||||
anonymize: anonymize ? 1 : 0
|
||||
});
|
||||
},
|
||||
|
||||
listDiagnostics: callListDiagnostics,
|
||||
downloadDiagnostic: function(name) {
|
||||
return callDownloadDiagnostic({ name: name });
|
||||
},
|
||||
deleteDiagnostic: function(name) {
|
||||
return callDeleteDiagnostic({ name: name });
|
||||
},
|
||||
runDiagnosticTest: function(test) {
|
||||
return callRunDiagnosticTest({ test: test });
|
||||
},
|
||||
|
||||
uploadDiagnostics: function(name) {
|
||||
return callUploadDiagnostics({ name: name });
|
||||
},
|
||||
|
||||
formatBytes: function(bytes) {
|
||||
if (!bytes || bytes <= 0)
|
||||
return '0 B';
|
||||
var units = ['B', 'KB', 'MB', 'GB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
|
||||
},
|
||||
|
||||
remoteStatus: callRemoteStatus,
|
||||
remoteInstall: callRemoteInstall,
|
||||
remoteConfigure: function(data) {
|
||||
return callRemoteConfigure(data);
|
||||
},
|
||||
remoteCredentials: callRemoteGetCredentials,
|
||||
remoteServiceAction: function(action) {
|
||||
return callRemoteServiceAction({ action: action });
|
||||
},
|
||||
remoteSaveSettings: function(data) {
|
||||
return callRemoteSaveSettings(data);
|
||||
}
|
||||
});
|
||||
|
||||
@ -7,8 +7,13 @@
|
||||
var api = L.require('system-hub.api');
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var self = this;
|
||||
load: function() {
|
||||
return api.listDiagnostics();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
this.currentArchives = (data && data.archives) || [];
|
||||
var archives = this.currentArchives;
|
||||
|
||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
@ -59,11 +64,7 @@ return view.extend({
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📁'), 'Archives Récentes' ])
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body', 'id': 'archives-list' }, [
|
||||
this.renderArchiveItem('diagnostic_20241220_154500.tar.gz', '256 KB', '2024-12-20 15:45:00'),
|
||||
this.renderArchiveItem('diagnostic_20241219_093000.tar.gz', '312 KB', '2024-12-19 09:30:00'),
|
||||
this.renderArchiveItem('diagnostic_20241218_110000.tar.gz', '298 KB', '2024-12-18 11:00:00')
|
||||
])
|
||||
E('div', { 'class': 'sh-card-body', 'id': 'archives-list' }, this.renderArchiveList(archives))
|
||||
]),
|
||||
|
||||
// Test Results
|
||||
@ -113,7 +114,19 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
renderArchiveItem: function(name, size, date) {
|
||||
renderArchiveList: function(archives) {
|
||||
if (!archives.length) {
|
||||
return [
|
||||
E('div', { 'style': 'text-align:center; color:#707080; padding:24px;' }, 'Aucune archive disponible')
|
||||
];
|
||||
}
|
||||
return archives.map(this.renderArchiveItem, this);
|
||||
},
|
||||
|
||||
renderArchiveItem: function(archive) {
|
||||
var name = archive.name || '';
|
||||
var size = api.formatBytes(archive.size || 0);
|
||||
var date = archive.created_at || '';
|
||||
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px; background: #1a1a24; border-radius: 8px; margin-bottom: 10px;' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
|
||||
E('span', { 'style': 'font-size: 20px;' }, '📦'),
|
||||
@ -123,8 +136,16 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||
E('button', { 'class': 'sh-btn', 'style': 'padding: 6px 10px; font-size: 10px;' }, '📥 Télécharger'),
|
||||
E('button', { 'class': 'sh-btn', 'style': 'padding: 6px 10px; font-size: 10px;' }, '☁️ Envoyer')
|
||||
E('button', {
|
||||
'class': 'sh-btn',
|
||||
'style': 'padding: 6px 10px; font-size: 10px;',
|
||||
'click': L.bind(this.downloadArchive, this, name)
|
||||
}, '📥 Télécharger'),
|
||||
E('button', {
|
||||
'class': 'sh-btn',
|
||||
'style': 'padding: 6px 10px; font-size: 10px; background:#321616;',
|
||||
'click': L.bind(this.deleteArchive, this, name)
|
||||
}, '🗑️ Supprimer')
|
||||
])
|
||||
]);
|
||||
},
|
||||
@ -140,49 +161,116 @@ return view.extend({
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
|
||||
api.callCollectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(function(result) {
|
||||
api.collectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(L.bind(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', {}, '✅ Archive créée: ' + result.file + ' (' + api.formatBytes(result.size) + ')'), 'success');
|
||||
this.refreshArchives();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, '❌ Erreur lors de la collecte'), 'error');
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
uploadDiagnostics: function() {
|
||||
ui.addNotification(null, E('p', {}, '⚠️ Fonctionnalité non configurée. Configurez l\'URL d\'upload dans les paramètres.'), 'warning');
|
||||
var archives = this.currentArchives || [];
|
||||
if (!archives.length) {
|
||||
ui.addNotification(null, E('p', {}, 'Aucune archive à envoyer'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
var latest = archives[0];
|
||||
ui.showModal(_('Upload Support'), [
|
||||
E('p', {}, 'Envoi de ' + latest.name + '…'),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
|
||||
api.uploadDiagnostics(latest.name).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, '☁️ Archive envoyée au support (' + (result.status || 'OK') + ')'), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, '❌ Upload impossible: ' + ((result && result.error) || 'Erreur inconnue')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
runTest: function(type) {
|
||||
var resultsDiv = document.getElementById('test-results');
|
||||
|
||||
resultsDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><div class="spinning"></div><div style="margin-top: 12px;">Test en cours...</div></div>';
|
||||
|
||||
// Simulate test
|
||||
setTimeout(function() {
|
||||
var results = {
|
||||
'connectivity': { status: 'ok', message: 'WAN connecté, DNS fonctionnel', details: 'Ping: 8.8.8.8 - 12ms' },
|
||||
'dns': { status: 'ok', message: 'Résolution DNS OK', details: 'google.com → 142.250.185.78' },
|
||||
'latency': { status: 'warning', message: 'Latence élevée', details: 'Google: 45ms (seuil: 30ms)' },
|
||||
'disk': { status: 'ok', message: 'Disque OK', details: 'Lecture: 25 MB/s, Écriture: 18 MB/s' },
|
||||
'firewall': { status: 'ok', message: '127 règles actives', details: 'INPUT: 23, FORWARD: 89, OUTPUT: 15' },
|
||||
'wifi': { status: 'ok', message: '2 radios actives', details: '2.4GHz: 8 clients, 5GHz: 4 clients' }
|
||||
};
|
||||
|
||||
var r = results[type] || { status: 'ok', message: 'Test complété', details: '' };
|
||||
var color = r.status === 'ok' ? '#22c55e' : (r.status === 'warning' ? '#f59e0b' : '#ef4444');
|
||||
var icon = r.status === 'ok' ? '✅' : (r.status === 'warning' ? '⚠️' : '❌');
|
||||
|
||||
api.runDiagnosticTest(type).then(function(result) {
|
||||
var color = result.success ? '#22c55e' : '#ef4444';
|
||||
var bg = result.success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
|
||||
var icon = result.success ? '✅' : '❌';
|
||||
resultsDiv.innerHTML = '';
|
||||
resultsDiv.appendChild(E('div', { 'style': 'padding: 20px; background: rgba(' + (r.status === 'ok' ? '34,197,94' : '245,158,11') + ', 0.1); border-radius: 10px; border-left: 3px solid ' + color }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 8px;' }, [
|
||||
E('span', { 'style': 'font-size: 24px;' }, icon),
|
||||
E('span', { 'style': 'font-size: 16px; font-weight: 600;' }, r.message)
|
||||
resultsDiv.appendChild(E('div', { 'style': 'padding: 18px; border-radius: 10px; border-left: 3px solid ' + color + '; background: ' + bg }, [
|
||||
E('div', { 'style': 'display:flex; align-items:center; gap:10px;' }, [
|
||||
E('span', { 'style': 'font-size:24px;' }, icon),
|
||||
E('div', { 'style': 'font-weight:600;' }, (result.test || type) + ' - ' + (result.success ? 'Réussi' : 'Échec'))
|
||||
]),
|
||||
E('div', { 'style': 'font-size: 12px; color: #a0a0b0; margin-left: 36px;' }, r.details)
|
||||
E('pre', { 'style': 'margin-top:12px; font-size:12px; white-space:pre-wrap;' }, result.output || '')
|
||||
]));
|
||||
}, 1500);
|
||||
}).catch(function(err) {
|
||||
resultsDiv.innerHTML = '';
|
||||
resultsDiv.appendChild(E('div', { 'class': 'sh-alert error' }, [
|
||||
E('div', { 'class': 'sh-alert-title' }, 'Erreur'),
|
||||
E('div', {}, err.message || err)
|
||||
]));
|
||||
});
|
||||
},
|
||||
|
||||
downloadArchive: function(name) {
|
||||
ui.showModal(_('Téléchargement…'), [
|
||||
E('p', {}, 'Préparation de ' + name)
|
||||
]);
|
||||
|
||||
api.downloadDiagnostic(name).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (!result.success || !result.data) {
|
||||
ui.addNotification(null, E('p', {}, '❌ Téléchargement impossible'), 'error');
|
||||
return;
|
||||
}
|
||||
var link = document.createElement('a');
|
||||
link.href = 'data:application/gzip;base64,' + result.data;
|
||||
link.download = result.name || name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deleteArchive: function(name) {
|
||||
if (!confirm(_('Supprimer ') + name + ' ?')) return;
|
||||
api.deleteDiagnostic(name).then(L.bind(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', {}, '🗑️ Archive supprimée'), 'info');
|
||||
this.refreshArchives();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, '❌ Suppression impossible'), 'error');
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
refreshArchives: function() {
|
||||
api.listDiagnostics().then(L.bind(function(data) {
|
||||
this.currentArchives = data.archives || [];
|
||||
var list = document.getElementById('archives-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
(this.renderArchiveList(this.currentArchives)).forEach(function(node) {
|
||||
list.appendChild(node);
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -5,32 +5,13 @@
|
||||
|
||||
var api = L.require('system-hub.api');
|
||||
|
||||
// Stub: Get remote access config (planned feature)
|
||||
function getRemoteConfig() {
|
||||
return Promise.resolve({
|
||||
rustdesk_enabled: false,
|
||||
rustdesk_installed: false,
|
||||
rustdesk_id: null,
|
||||
allow_unattended: false,
|
||||
require_approval: true,
|
||||
notify_on_connect: true,
|
||||
support: {
|
||||
provider: 'CyberMind.fr',
|
||||
email: 'support@cybermind.fr',
|
||||
phone: '+33 1 23 45 67 89',
|
||||
website: 'https://cybermind.fr'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return getRemoteConfig();
|
||||
return api.remoteStatus();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var remote = data;
|
||||
var self = this;
|
||||
render: function(remote) {
|
||||
this.remote = remote || {};
|
||||
|
||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||
@ -39,37 +20,43 @@ return view.extend({
|
||||
E('div', { 'class': 'sh-card sh-remote-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🖥️'), 'RustDesk - Assistance à Distance' ]),
|
||||
E('div', { 'class': 'sh-card-badge' }, remote.rustdesk_enabled ? 'Actif' : 'Inactif')
|
||||
E('div', { 'class': 'sh-card-badge' }, remote.enabled ? 'Actif' : 'Inactif')
|
||||
]),
|
||||
E('div', { 'class': 'sh-card-body' }, [
|
||||
// RustDesk ID
|
||||
E('div', { 'class': 'sh-remote-id' }, [
|
||||
E('div', { 'class': 'sh-remote-id-icon' }, '🖥️'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'sh-remote-id-value' }, remote.rustdesk_id || '--- --- ---'),
|
||||
E('div', { 'class': 'sh-remote-id-value', 'id': 'remote-id-value' }, remote.id || '--- --- ---'),
|
||||
E('div', { 'class': 'sh-remote-id-label' }, 'ID RustDesk - Communiquez ce code au support')
|
||||
])
|
||||
]),
|
||||
|
||||
// Settings
|
||||
this.renderToggle('🔒', 'Accès sans surveillance', 'Permettre la connexion sans approbation', remote.allow_unattended),
|
||||
this.renderToggle('✅', 'Approbation requise', 'Confirmer chaque connexion entrante', remote.require_approval),
|
||||
this.renderToggle('🔔', 'Notification de connexion', 'Recevoir une alerte à chaque session', remote.notify_on_connect),
|
||||
this.renderToggle('🔒', 'Accès sans surveillance', 'Permettre la connexion sans approbation', remote.allow_unattended, 'allow_unattended'),
|
||||
this.renderToggle('✅', 'Approbation requise', 'Confirmer chaque connexion entrante', remote.require_approval, 'require_approval'),
|
||||
this.renderToggle('🔔', 'Notification de connexion', 'Recevoir une alerte à chaque session', remote.notify_on_connect, 'notify_on_connect'),
|
||||
|
||||
// Status
|
||||
!remote.rustdesk_installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [
|
||||
!remote.installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [
|
||||
E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, '⚠️'),
|
||||
E('span', {}, 'RustDesk n\'est pas installé. '),
|
||||
E('a', { 'href': '#', 'style': 'color: #6366f1;' }, 'Installer maintenant')
|
||||
]) : E('span'),
|
||||
E('a', { 'href': '#', 'style': 'color: #6366f1;', 'click': L.bind(this.installRustdesk, this) }, 'Installer maintenant')
|
||||
]) : E('div', { 'style': 'padding: 10px; background: rgba(34,197,94,0.12); border-radius: 10px; margin-top: 16px;' }, [
|
||||
E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, remote.running ? '🟢' : '🟠'),
|
||||
E('span', {}, remote.running ? 'Service RustDesk en cours d\'exécution' : 'Service installé mais arrêté')
|
||||
]),
|
||||
|
||||
// Actions
|
||||
E('div', { 'class': 'sh-btn-group' }, [
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-primary',
|
||||
'click': L.bind(this.startSession, this, 'rustdesk')
|
||||
}, [ '🚀 Démarrer Session' ]),
|
||||
E('button', { 'class': 'sh-btn' }, [ '⚙️ Configurer RustDesk' ])
|
||||
'click': L.bind(this.showCredentials, this)
|
||||
}, [ '🔑 Identifiants' ]),
|
||||
E('button', {
|
||||
'class': 'sh-btn',
|
||||
'click': L.bind(this.toggleService, this)
|
||||
}, [ remote.running ? '⏹️ Arrêter' : '▶️ Démarrer' ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
@ -96,7 +83,7 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// Support Contact
|
||||
// Support Contact (static)
|
||||
E('div', { 'class': 'sh-card' }, [
|
||||
E('div', { 'class': 'sh-card-header' }, [
|
||||
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📞'), 'Contact Support' ])
|
||||
@ -105,24 +92,20 @@ return view.extend({
|
||||
E('div', { 'class': 'sh-sysinfo-grid' }, [
|
||||
E('div', { 'class': 'sh-sysinfo-item' }, [
|
||||
E('span', { 'class': 'sh-sysinfo-label' }, 'Fournisseur'),
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.provider || 'N/A')
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, 'CyberMind.fr')
|
||||
]),
|
||||
E('div', { 'class': 'sh-sysinfo-item' }, [
|
||||
E('span', { 'class': 'sh-sysinfo-label' }, 'Email'),
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.email || 'N/A')
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, 'support@cybermind.fr')
|
||||
]),
|
||||
E('div', { 'class': 'sh-sysinfo-item' }, [
|
||||
E('span', { 'class': 'sh-sysinfo-label' }, 'Téléphone'),
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.phone || 'N/A')
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, '+33 1 23 45 67 89')
|
||||
]),
|
||||
E('div', { 'class': 'sh-sysinfo-item' }, [
|
||||
E('span', { 'class': 'sh-sysinfo-label' }, 'Website'),
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.website || 'N/A')
|
||||
E('span', { 'class': 'sh-sysinfo-value' }, 'https://cybermind.fr')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-btn-group' }, [
|
||||
E('button', { 'class': 'sh-btn sh-btn-primary' }, [ '🎫 Ouvrir un Ticket' ]),
|
||||
E('button', { 'class': 'sh-btn' }, [ '📚 Documentation' ])
|
||||
])
|
||||
])
|
||||
])
|
||||
@ -131,7 +114,7 @@ return view.extend({
|
||||
return view;
|
||||
},
|
||||
|
||||
renderToggle: function(icon, label, desc, enabled) {
|
||||
renderToggle: function(icon, label, desc, enabled, field) {
|
||||
return E('div', { 'class': 'sh-toggle' }, [
|
||||
E('div', { 'class': 'sh-toggle-info' }, [
|
||||
E('span', { 'class': 'sh-toggle-icon' }, icon),
|
||||
@ -142,22 +125,88 @@ return view.extend({
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'sh-toggle-switch' + (enabled ? ' active' : ''),
|
||||
'click': function(ev) { ev.target.classList.toggle('active'); }
|
||||
'data-field': field,
|
||||
'click': L.bind(function(ev) {
|
||||
ev.target.classList.toggle('active');
|
||||
this.saveSettings();
|
||||
}, this)
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
||||
startSession: function(type) {
|
||||
ui.showModal(_('Démarrage Session'), [
|
||||
E('p', {}, 'Démarrage de la session ' + type + '...'),
|
||||
showCredentials: function() {
|
||||
ui.showModal(_('Identifiants RustDesk'), [
|
||||
E('p', {}, 'Récupération en cours…'),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
|
||||
// Stub: Remote session not yet implemented
|
||||
setTimeout(function() {
|
||||
api.remoteCredentials().then(function(result) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, '⚠️ Remote session feature coming soon'), 'info');
|
||||
}, 1000);
|
||||
ui.showModal(_('Identifiants RustDesk'), [
|
||||
E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')),
|
||||
E('div', { 'style': 'font-size:18px;' }, 'Mot de passe: ' + (result.password || '---')),
|
||||
E('div', { 'class': 'sh-btn-group', 'style': 'margin-top:16px;' }, [
|
||||
E('button', { 'class': 'sh-btn sh-btn-primary', 'click': ui.hideModal }, 'Fermer')
|
||||
])
|
||||
]);
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
toggleService: function() {
|
||||
if (!this.remote || !this.remote.installed) return;
|
||||
var action = this.remote.running ? 'stop' : 'start';
|
||||
api.remoteServiceAction(action).then(L.bind(function(res) {
|
||||
if (res.success) {
|
||||
this.reload();
|
||||
ui.addNotification(null, E('p', {}, '✅ ' + action), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, res.error || 'Action impossible'), 'error');
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
installRustdesk: function(ev) {
|
||||
ev.preventDefault();
|
||||
ui.showModal(_('Installation'), [
|
||||
E('p', {}, 'Installation de RustDesk…'),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
api.remoteInstall().then(L.bind(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', {}, result.message || 'Installé'), 'info');
|
||||
this.reload();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.error || 'Installation impossible'), 'error');
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
var allow = document.querySelector('[data-field="allow_unattended"]').classList.contains('active') ? 1 : 0;
|
||||
var require = document.querySelector('[data-field="require_approval"]').classList.contains('active') ? 1 : 0;
|
||||
var notify = document.querySelector('[data-field="notify_on_connect"]').classList.contains('active') ? 1 : 0;
|
||||
|
||||
api.remoteSaveSettings({
|
||||
allow_unattended: allow,
|
||||
require_approval: require,
|
||||
notify_on_connect: notify
|
||||
});
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.load().then(L.bind(function(data) {
|
||||
var node = this.render(data);
|
||||
var root = document.querySelector('.system-hub-dashboard');
|
||||
if (root && root.parentNode) {
|
||||
root.parentNode.replaceChild(node, root);
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -6,6 +6,16 @@
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
DIAG_DIR="/tmp/system-hub/diagnostics"
|
||||
mkdir -p "$DIAG_DIR"
|
||||
|
||||
safe_filename() {
|
||||
local name="$1"
|
||||
name="${name##*/}"
|
||||
name="${name//../}"
|
||||
echo "$name"
|
||||
}
|
||||
|
||||
# Get comprehensive system status
|
||||
status() {
|
||||
json_init
|
||||
@ -560,6 +570,470 @@ get_storage() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Collect diagnostic data into archive
|
||||
collect_diagnostics() {
|
||||
read input
|
||||
[ -n "$input" ] && json_load "$input"
|
||||
|
||||
local include_logs include_config include_network anonymize
|
||||
json_get_var include_logs include_logs 2>/dev/null
|
||||
json_get_var include_config include_config 2>/dev/null
|
||||
json_get_var include_network include_network 2>/dev/null
|
||||
json_get_var anonymize anonymize 2>/dev/null
|
||||
|
||||
[ -z "$include_logs" ] && include_logs=1
|
||||
[ -z "$include_config" ] && include_config=1
|
||||
[ -z "$include_network" ] && include_network=1
|
||||
[ -z "$anonymize" ] && anonymize=0
|
||||
|
||||
local timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
local workdir="$DIAG_DIR/work-$timestamp"
|
||||
mkdir -p "$workdir"
|
||||
|
||||
# System info
|
||||
{
|
||||
echo "=== System Information ==="
|
||||
uname -a
|
||||
echo
|
||||
echo "--- CPU ---"
|
||||
cat /proc/cpuinfo
|
||||
echo
|
||||
echo "--- Memory ---"
|
||||
cat /proc/meminfo
|
||||
echo
|
||||
echo "--- Disk ---"
|
||||
df -h
|
||||
echo
|
||||
echo "--- Uptime ---"
|
||||
uptime
|
||||
} >"$workdir/sysinfo.txt"
|
||||
|
||||
# Logs
|
||||
if [ "$include_logs" = "1" ]; then
|
||||
logread 2>/dev/null >"$workdir/system.log" || true
|
||||
dmesg 2>/dev/null >"$workdir/kernel.log" || true
|
||||
fi
|
||||
|
||||
# Configs
|
||||
if [ "$include_config" = "1" ]; then
|
||||
mkdir -p "$workdir/config"
|
||||
sysupgrade -b "$workdir/config/backup.tar.gz" >/dev/null 2>&1 || true
|
||||
for conf in /etc/config/*; do
|
||||
[ -f "$conf" ] || continue
|
||||
local dest="$workdir/config/$(basename "$conf")"
|
||||
if [ "$anonymize" = "1" ]; then
|
||||
grep -viE "(password|passwd|secret|key|token|psk|ipaddr|macaddr)" "$conf" >"$dest" 2>/dev/null || cp "$conf" "$dest"
|
||||
else
|
||||
cp "$conf" "$dest"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Network info
|
||||
if [ "$include_network" = "1" ]; then
|
||||
{
|
||||
echo "=== Interfaces ==="
|
||||
ip addr
|
||||
echo
|
||||
echo "=== Routes ==="
|
||||
ip route
|
||||
echo
|
||||
echo "=== Firewall ==="
|
||||
iptables -L -n -v 2>/dev/null || true
|
||||
echo
|
||||
echo "=== DNS ==="
|
||||
cat /etc/resolv.conf 2>/dev/null
|
||||
echo
|
||||
echo "=== Connectivity (8.8.8.8) ==="
|
||||
ping -c 3 -W 2 8.8.8.8 2>&1
|
||||
echo
|
||||
echo "=== Connectivity (1.1.1.1) ==="
|
||||
ping -c 3 -W 2 1.1.1.1 2>&1
|
||||
} >"$workdir/network.txt"
|
||||
fi
|
||||
|
||||
local archive_name="diagnostics-$(hostname)-$timestamp.tar.gz"
|
||||
local archive_path="$DIAG_DIR/$archive_name"
|
||||
|
||||
tar -czf "$archive_path" -C "$workdir" . >/dev/null 2>&1 || {
|
||||
rm -rf "$workdir"
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "archive_failed"
|
||||
json_dump
|
||||
return
|
||||
}
|
||||
|
||||
rm -rf "$workdir"
|
||||
|
||||
local size=$(stat -c%s "$archive_path" 2>/dev/null || stat -f%z "$archive_path")
|
||||
local created=$(date -r "$archive_path" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "file" "$archive_name"
|
||||
json_add_int "size" "$size"
|
||||
json_add_string "created_at" "$created"
|
||||
json_dump
|
||||
}
|
||||
|
||||
list_diagnostics() {
|
||||
json_init
|
||||
json_add_array "archives"
|
||||
|
||||
for file in $(ls -t "$DIAG_DIR"/diagnostics-*.tar.gz 2>/dev/null | head -n 50); do
|
||||
[ -f "$file" ] || continue
|
||||
local size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
|
||||
local created=$(date -r "$file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "")
|
||||
json_add_object
|
||||
json_add_string "name" "$(basename "$file")"
|
||||
json_add_int "size" "$size"
|
||||
json_add_string "created_at" "$created"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_add_boolean "success" 1
|
||||
json_dump
|
||||
}
|
||||
|
||||
download_diagnostic() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var name name
|
||||
|
||||
local safe="$(safe_filename "$name")"
|
||||
local file="$DIAG_DIR/$safe"
|
||||
json_init
|
||||
|
||||
if [ -z "$safe" ] || [ ! -f "$file" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "not_found"
|
||||
json_dump
|
||||
return
|
||||
}
|
||||
|
||||
local data=$(base64 "$file" 2>/dev/null)
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "name" "$safe"
|
||||
json_add_string "data" "$data"
|
||||
json_dump
|
||||
}
|
||||
|
||||
delete_diagnostic() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var name name
|
||||
local safe="$(safe_filename "$name")"
|
||||
local file="$DIAG_DIR/$safe"
|
||||
|
||||
json_init
|
||||
if [ -n "$safe" ] && [ -f "$file" ]; then
|
||||
rm -f "$file"
|
||||
json_add_boolean "success" 1
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "not_found"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
run_diagnostic_test() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var test test
|
||||
|
||||
local output=""
|
||||
local success=1
|
||||
|
||||
case "$test" in
|
||||
connectivity)
|
||||
output="$(ping -c 3 -W 2 8.8.8.8 2>&1; echo; ping -c 3 -W 2 1.1.1.1 2>&1)"
|
||||
;;
|
||||
dns)
|
||||
if command -v nslookup >/dev/null 2>&1; then
|
||||
output="$(nslookup openwrt.org 2>&1)"
|
||||
else
|
||||
output="nslookup unavailable"
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
latency)
|
||||
output="$(ping -c 5 -W 2 google.com 2>&1)"
|
||||
;;
|
||||
disk)
|
||||
local testfile="/tmp/system-hub-disk-test"
|
||||
if dd if=/dev/zero of="$testfile" bs=1M count=4 conv=fsync 2>&1; then
|
||||
sync
|
||||
rm -f "$testfile"
|
||||
output="Disk write test completed (4MB)."
|
||||
else
|
||||
output="Disk test failed."
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
firewall)
|
||||
output="$(iptables -L -n -v 2>&1)"
|
||||
;;
|
||||
wifi)
|
||||
if command -v iwinfo >/dev/null 2>&1; then
|
||||
output="$(iwinfo 2>&1)"
|
||||
else
|
||||
output="iwinfo not available"
|
||||
success=0
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
output="unknown test"
|
||||
success=0
|
||||
;;
|
||||
esac
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" "$success"
|
||||
json_add_string "test" "${test:-unknown}"
|
||||
json_add_string "output" "$output"
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_status() {
|
||||
local section="system-hub.remote"
|
||||
local enabled=$(uci -q get $section.rustdesk_enabled || echo 0)
|
||||
local relay_server=$(uci -q get $section.rustdesk_server || echo "")
|
||||
local relay_key=$(uci -q get $section.rustdesk_key || echo "")
|
||||
local stored_id=$(uci -q get $section.rustdesk_id || echo "")
|
||||
local stored_password=$(uci -q get $section.rustdesk_password || echo "")
|
||||
local allow_unattended=$(uci -q get $section.allow_unattended || echo 0)
|
||||
local require_approval=$(uci -q get $section.require_approval || echo 1)
|
||||
local notify_on_connect=$(uci -q get $section.notify_on_connect || echo 1)
|
||||
|
||||
local installed=0
|
||||
command -v rustdesk >/dev/null 2>&1 && installed=1
|
||||
|
||||
local running=0
|
||||
if [ -x /etc/init.d/rustdesk ]; then
|
||||
/etc/init.d/rustdesk status >/dev/null 2>&1 && running=1
|
||||
fi
|
||||
|
||||
json_init
|
||||
json_add_boolean "installed" "$installed"
|
||||
json_add_boolean "running" "$running"
|
||||
json_add_boolean "enabled" "$enabled"
|
||||
json_add_string "server" "$relay_server"
|
||||
json_add_string "key" "$relay_key"
|
||||
json_add_string "id" "$stored_id"
|
||||
json_add_string "password" "$stored_password"
|
||||
json_add_boolean "allow_unattended" "$allow_unattended"
|
||||
json_add_boolean "require_approval" "$require_approval"
|
||||
json_add_boolean "notify_on_connect" "$notify_on_connect"
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_install() {
|
||||
json_init
|
||||
if command -v rustdesk >/dev/null 2>&1; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "already_installed"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v opkg >/dev/null 2>&1; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "opkg_missing"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
opkg update >/tmp/rustdesk-install.log 2>&1
|
||||
if opkg install rustdesk >>/tmp/rustdesk-install.log 2>&1; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "RustDesk installed"
|
||||
else
|
||||
local err="$(tail -n 20 /tmp/rustdesk-install.log 2>/dev/null)"
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "${err:-install_failed}"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_configure() {
|
||||
read input
|
||||
json_load "$input"
|
||||
local section="system-hub.remote"
|
||||
local server key enabled
|
||||
json_get_var server relay_server
|
||||
json_get_var key relay_key
|
||||
json_get_var enabled rustdesk_enabled
|
||||
|
||||
[ -n "$server" ] && uci set $section.rustdesk_server="$server"
|
||||
[ -n "$key" ] && uci set $section.rustdesk_key="$key"
|
||||
[ -n "$enabled" ] && uci set $section.rustdesk_enabled="$enabled"
|
||||
uci commit system-hub
|
||||
|
||||
if [ -n "$server" ] || [ -n "$key" ]; then
|
||||
mkdir -p /etc/rustdesk
|
||||
cat > /etc/rustdesk/config.toml <<EOF
|
||||
[relay]
|
||||
server = "${server:-$(uci -q get $section.rustdesk_server)}"
|
||||
|
||||
[options]
|
||||
key = "${key:-$(uci -q get $section.rustdesk_key)}"
|
||||
auto_start = true
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ -x /etc/init.d/rustdesk ] && [ "${enabled:-0}" = "1" ]; then
|
||||
/etc/init.d/rustdesk enable >/dev/null 2>&1 || true
|
||||
/etc/init.d/rustdesk restart >/dev/null 2>&1 || true
|
||||
elif [ -x /etc/init.d/rustdesk ]; then
|
||||
/etc/init.d/rustdesk stop >/dev/null 2>&1 || true
|
||||
/etc/init.d/rustdesk disable >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_get_credentials() {
|
||||
local section="system-hub.remote"
|
||||
local rid="" rpass=""
|
||||
if command -v rustdesk >/dev/null 2>&1; then
|
||||
rid=$(rustdesk --get-id 2>/dev/null || echo "")
|
||||
rpass=$(rustdesk --password 2>/dev/null || echo "")
|
||||
fi
|
||||
[ -z "$rid" ] && rid=$(uci -q get $section.rustdesk_id || echo "")
|
||||
[ -z "$rpass" ] && rpass=$(uci -q get $section.rustdesk_password || echo "")
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "id" "$rid"
|
||||
json_add_string "password" "$rpass"
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_service_action() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var action action
|
||||
|
||||
json_init
|
||||
if [ ! -x /etc/init.d/rustdesk ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "service_missing"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
start|stop|restart|enable|disable)
|
||||
if /etc/init.d/rustdesk "$action" >/dev/null 2>&1; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$action"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "action_failed"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "invalid_action"
|
||||
;;
|
||||
esac
|
||||
json_dump
|
||||
}
|
||||
|
||||
remote_save_settings() {
|
||||
read input
|
||||
json_load "$input"
|
||||
local section="system-hub.remote"
|
||||
local allow require notify
|
||||
json_get_var allow allow_unattended
|
||||
json_get_var require require_approval
|
||||
json_get_var notify notify_on_connect
|
||||
|
||||
[ -n "$allow" ] && uci set $section.allow_unattended="$allow"
|
||||
[ -n "$require" ] && uci set $section.require_approval="$require"
|
||||
[ -n "$notify" ] && uci set $section.notify_on_connect="$notify"
|
||||
uci commit system-hub
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_dump
|
||||
}
|
||||
|
||||
upload_diagnostics() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var name name
|
||||
|
||||
local upload_url=$(uci -q get system-hub.diagnostics.upload_url)
|
||||
local upload_token=$(uci -q get system-hub.diagnostics.upload_token)
|
||||
|
||||
json_init
|
||||
|
||||
if [ -z "$upload_url" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "upload_url_missing"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "curl_missing"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local safe="$(safe_filename "$name")"
|
||||
if [ -z "$safe" ]; then
|
||||
# Use latest archive if not specified
|
||||
safe="$(ls -t "$DIAG_DIR"/diagnostics-*.tar.gz 2>/dev/null | head -n1)"
|
||||
[ -n "$safe" ] && safe="$(basename "$safe")"
|
||||
fi
|
||||
|
||||
local file="$DIAG_DIR/$safe"
|
||||
if [ -z "$safe" ] || [ ! -f "$file" ]; then
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "archive_not_found"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
local response
|
||||
if [ -n "$upload_token" ]; then
|
||||
response=$(curl -s -S -o /tmp/system-hub-upload.log -w "%{http_code}" \
|
||||
-H "Authorization: Bearer $upload_token" \
|
||||
-F "file=@$file" \
|
||||
-F "hostname=$(hostname)" \
|
||||
-F "timestamp=$(date +%s)" \
|
||||
"$upload_url" 2>&1)
|
||||
else
|
||||
response=$(curl -s -S -o /tmp/system-hub-upload.log -w "%{http_code}" \
|
||||
-F "file=@$file" \
|
||||
-F "hostname=$(hostname)" \
|
||||
-F "timestamp=$(date +%s)" \
|
||||
"$upload_url" 2>&1)
|
||||
fi
|
||||
|
||||
local curl_exit=$?
|
||||
local body="$(cat /tmp/system-hub-upload.log 2>/dev/null || true)"
|
||||
rm -f /tmp/system-hub-upload.log
|
||||
|
||||
if [ $curl_exit -eq 0 ]; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "status" "$response"
|
||||
json_add_string "body" "$body"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "upload_failed"
|
||||
json_add_string "details" "$body"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Get settings
|
||||
get_settings() {
|
||||
json_init
|
||||
@ -770,6 +1244,31 @@ case "$1" in
|
||||
"disk_critical": 95,
|
||||
"temp_warning": 70,
|
||||
"temp_critical": 85
|
||||
},
|
||||
"collect_diagnostics": {
|
||||
"include_logs": 1,
|
||||
"include_config": 1,
|
||||
"include_network": 1,
|
||||
"anonymize": 1
|
||||
},
|
||||
"list_diagnostics": {},
|
||||
"download_diagnostic": { "name": "string" },
|
||||
"delete_diagnostic": { "name": "string" },
|
||||
"run_diagnostic_test": { "test": "string" },
|
||||
"upload_diagnostics": { "name": "string" },
|
||||
"remote_status": {},
|
||||
"remote_install": {},
|
||||
"remote_configure": {
|
||||
"relay_server": "string",
|
||||
"relay_key": "string",
|
||||
"rustdesk_enabled": 1
|
||||
},
|
||||
"remote_get_credentials": {},
|
||||
"remote_service_action": { "action": "start|stop|restart|enable|disable" },
|
||||
"remote_save_settings": {
|
||||
"allow_unattended": 0,
|
||||
"require_approval": 1,
|
||||
"notify_on_connect": 1
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@ -790,6 +1289,18 @@ EOF
|
||||
get_storage) get_storage ;;
|
||||
get_settings) get_settings ;;
|
||||
save_settings) save_settings ;;
|
||||
collect_diagnostics) collect_diagnostics ;;
|
||||
list_diagnostics) list_diagnostics ;;
|
||||
download_diagnostic) download_diagnostic ;;
|
||||
delete_diagnostic) delete_diagnostic ;;
|
||||
run_diagnostic_test) run_diagnostic_test ;;
|
||||
upload_diagnostics) upload_diagnostics ;;
|
||||
remote_status) remote_status ;;
|
||||
remote_install) remote_install ;;
|
||||
remote_configure) remote_configure ;;
|
||||
remote_get_credentials) remote_get_credentials ;;
|
||||
remote_service_action) remote_service_action ;;
|
||||
remote_save_settings) remote_save_settings ;;
|
||||
*)
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user