diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a4ac868b..6b0b7447 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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)" ] } } diff --git a/luci-app-network-modes/README.md b/luci-app-network-modes/README.md index 17c721ba..5f1981c8 100644 --- a/luci-app-network-modes/README.md +++ b/luci-app-network-modes/README.md @@ -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) diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js index acc1d87f..304a5102 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js @@ -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' }, '🌐'), diff --git a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes index ff605e14..0057dc12 100755 --- a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes +++ b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes @@ -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 </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 </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 </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" <> "/etc/nginx/conf.d/vhost-$idx.conf" <> "/etc/nginx/conf.d/vhost-$idx.conf" </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"}' ;; diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js index 99aa2abe..b559bdab 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js @@ -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); + } }); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js index 463779a7..03d67adc 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js @@ -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 = '
Test en cours...
'; - - // 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, diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js index 6c456892..fdc150bf 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js @@ -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, diff --git a/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index b44307c0..ecb0f771 100755 --- a/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -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 </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