feat: Add HTTP health checks, portal speedtest, and fix cert detection
- metablogizer: Add HTTP health checks for backend (uhttpd) and frontend (HAProxy) - metablogizer: Fix BusyBox-compatible certificate expiry detection using openssl checkend - secubox-portal: Add speed test widget with ping/download/upload measurement - tor-shield: Fix settings save ensuring UCI sections exist - cdn-cache: UI improvements and restructure - streamlit: Fix port conflict (sappix now uses 8503) - secubox-core: Add proxy mode detection - security-threats: Dashboard improvements - haproxy: Init.d and Makefile updates PKG_RELEASE bumps: - luci-app-cdn-cache: 3 - luci-app-metablogizer: 2 - luci-app-secubox-portal: 2 - luci-app-secubox-security-threats: 2 - luci-app-secubox: 4 - luci-app-streamlit: 9 - luci-app-tor-shield: 2 - secubox-app-haproxy: 23 - secubox-core: 6 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
14af23774a
commit
906bf6f549
27
CLAUDE.md
27
CLAUDE.md
@ -43,6 +43,33 @@ When checking if a port is listening, use this order of fallbacks:
|
||||
|
||||
## Build & Sync Workflow
|
||||
|
||||
### CRITICAL: Sync Local Feed Before Building
|
||||
- **ALWAYS sync the local-feed before building packages from edited source trees**
|
||||
- The build system uses `secubox-tools/local-feed/` NOT `package/secubox/` directly
|
||||
- If you edit files in `package/secubox/<pkg>/`, those changes won't be built unless synced
|
||||
|
||||
**Before building after edits:**
|
||||
```bash
|
||||
# Option 1: Sync specific package to local-feed
|
||||
rsync -av --delete package/secubox/<package-name>/ secubox-tools/local-feed/<package-name>/
|
||||
|
||||
# Option 2: Sync all SecuBox packages
|
||||
for pkg in package/secubox/*/; do
|
||||
name=$(basename "$pkg")
|
||||
rsync -av --delete "$pkg" "secubox-tools/local-feed/$name/"
|
||||
done
|
||||
|
||||
# Then build
|
||||
./secubox-tools/local-build.sh build <package-name>
|
||||
```
|
||||
|
||||
**Quick deploy without rebuild (for RPCD/shell scripts):**
|
||||
```bash
|
||||
# Copy script directly to router for testing
|
||||
scp package/secubox/<pkg>/root/usr/libexec/rpcd/<script> root@192.168.255.1:/usr/libexec/rpcd/
|
||||
ssh root@192.168.255.1 '/etc/init.d/rpcd restart'
|
||||
```
|
||||
|
||||
### Local Feeds Hygiene
|
||||
- Clean and resync local feeds before build iterations when dependency drift is suspected
|
||||
- Prefer the repo helpers; avoid ad-hoc `rm` unless explicitly needed
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-cdn-cache
|
||||
PKG_VERSION:=0.5.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
'require baseclass';
|
||||
|
||||
var tabs = [
|
||||
{ id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'overview'] },
|
||||
{ id: 'cache', icon: '💾', label: _('Cache'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'cache'] },
|
||||
{ id: 'policies', icon: '🧭', label: _('Policies'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'policies'] },
|
||||
{ id: 'statistics', icon: '📊', label: _('Statistics'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'statistics'] },
|
||||
{ id: 'maintenance', icon: '🧹', label: _('Maintenance'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'maintenance'] },
|
||||
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'settings'] }
|
||||
{ id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'services', 'cdn-cache', 'overview'] },
|
||||
{ id: 'cache', icon: '💾', label: _('Cache'), path: ['admin', 'services', 'cdn-cache', 'cache'] },
|
||||
{ id: 'policies', icon: '🧭', label: _('Policies'), path: ['admin', 'services', 'cdn-cache', 'policies'] },
|
||||
{ id: 'statistics', icon: '📊', label: _('Statistics'), path: ['admin', 'services', 'cdn-cache', 'statistics'] },
|
||||
{ id: 'maintenance', icon: '🧹', label: _('Maintenance'), path: ['admin', 'services', 'cdn-cache', 'maintenance'] },
|
||||
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'services', 'cdn-cache', 'settings'] }
|
||||
];
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
@ -49,22 +49,23 @@ function formatUptime(seconds) {
|
||||
return minutes + 'm ' + (seconds % 60) + 's';
|
||||
}
|
||||
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: lang });
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callStats(),
|
||||
callCacheSize(),
|
||||
callTopDomains()
|
||||
L.resolveDefault(callStatus(), {}),
|
||||
L.resolveDefault(callStats(), {}),
|
||||
L.resolveDefault(callCacheSize(), {}),
|
||||
L.resolveDefault(callTopDomains(), { domains: [] })
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
// Initialize theme
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: lang });
|
||||
|
||||
var status = data[0] || {};
|
||||
var stats = data[1] || {};
|
||||
var cacheSize = data[2] || {};
|
||||
@ -204,5 +205,9 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'secubox-stat-label' }, meta)
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
|
||||
@ -138,11 +138,13 @@ return view.extend({
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '60';
|
||||
|
||||
return E('div', { 'class': 'cdn-settings-page' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
|
||||
CdnNav.renderTabs('settings'),
|
||||
m.render()
|
||||
]);
|
||||
return m.render().then(function(formEl) {
|
||||
return E('div', { 'class': 'cdn-settings-page' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
|
||||
CdnNav.renderTabs('settings'),
|
||||
formEl
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"admin/secubox/network/cdn-cache": {
|
||||
"admin/services/cdn-cache": {
|
||||
"title": "CDN Cache",
|
||||
"order": 30,
|
||||
"order": 35,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
@ -10,7 +10,7 @@
|
||||
"uci": {"cdn-cache": true}
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/overview": {
|
||||
"admin/services/cdn-cache/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
@ -18,7 +18,7 @@
|
||||
"path": "cdn-cache/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/cache": {
|
||||
"admin/services/cdn-cache/cache": {
|
||||
"title": "Cache",
|
||||
"order": 20,
|
||||
"action": {
|
||||
@ -26,7 +26,7 @@
|
||||
"path": "cdn-cache/cache"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/policies": {
|
||||
"admin/services/cdn-cache/policies": {
|
||||
"title": "Policies",
|
||||
"order": 30,
|
||||
"action": {
|
||||
@ -34,7 +34,7 @@
|
||||
"path": "cdn-cache/policies"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/statistics": {
|
||||
"admin/services/cdn-cache/statistics": {
|
||||
"title": "Statistics",
|
||||
"order": 40,
|
||||
"action": {
|
||||
@ -42,7 +42,7 @@
|
||||
"path": "cdn-cache/statistics"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/maintenance": {
|
||||
"admin/services/cdn-cache/maintenance": {
|
||||
"title": "Maintenance",
|
||||
"order": 50,
|
||||
"action": {
|
||||
@ -50,7 +50,7 @@
|
||||
"path": "cdn-cache/maintenance"
|
||||
}
|
||||
},
|
||||
"admin/secubox/network/cdn-cache/settings": {
|
||||
"admin/services/cdn-cache/settings": {
|
||||
"title": "Settings",
|
||||
"order": 90,
|
||||
"action": {
|
||||
|
||||
@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-metablogizer
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
|
||||
@ -776,31 +776,104 @@ get_public_ipv4() {
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# Helper: Check certificate expiry
|
||||
# Helper: Check certificate expiry (BusyBox compatible)
|
||||
check_cert_expiry() {
|
||||
local domain="$1"
|
||||
local cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||
local cert_file=""
|
||||
|
||||
if [ ! -f "$cert_file" ]; then
|
||||
# Check multiple possible certificate locations
|
||||
# 1. HAProxy LXC container certs
|
||||
if [ -f "/srv/lxc/haproxy/rootfs/srv/haproxy/certs/${domain}.pem" ]; then
|
||||
cert_file="/srv/lxc/haproxy/rootfs/srv/haproxy/certs/${domain}.pem"
|
||||
# 2. HAProxy host path (if not containerized)
|
||||
elif [ -f "/srv/haproxy/certs/${domain}.pem" ]; then
|
||||
cert_file="/srv/haproxy/certs/${domain}.pem"
|
||||
# 3. ACME shared certs
|
||||
elif [ -f "/usr/share/haproxy/certs/${domain}.pem" ]; then
|
||||
cert_file="/usr/share/haproxy/certs/${domain}.pem"
|
||||
# 4. ACME ECC certs
|
||||
elif [ -f "/etc/acme/${domain}_ecc/${domain}.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}_ecc/${domain}.cer"
|
||||
[ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||
# 5. ACME RSA certs
|
||||
elif [ -f "/etc/acme/${domain}/${domain}.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}/${domain}.cer"
|
||||
# 6. ACME fullchain
|
||||
elif [ -f "/etc/acme/${domain}_ecc/fullchain.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}_ecc/fullchain.cer"
|
||||
elif [ -f "/etc/acme/${domain}/fullchain.cer" ]; then
|
||||
cert_file="/etc/acme/${domain}/fullchain.cer"
|
||||
fi
|
||||
|
||||
if [ -f "$cert_file" ]; then
|
||||
local expiry_date
|
||||
expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
|
||||
if [ -n "$expiry_date" ]; then
|
||||
local expiry_epoch now_epoch days_left
|
||||
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
|
||||
now_epoch=$(date +%s)
|
||||
if [ -n "$expiry_epoch" ]; then
|
||||
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
|
||||
echo "$days_left"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ -z "$cert_file" ] || [ ! -f "$cert_file" ]; then
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
|
||||
# Use openssl x509 -checkend to determine days until expiry
|
||||
# This is BusyBox compatible and doesn't rely on date parsing
|
||||
local days=0
|
||||
|
||||
# Check if certificate is already expired
|
||||
if ! openssl x509 -checkend 0 -noout -in "$cert_file" >/dev/null 2>&1; then
|
||||
echo "-1"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Binary search to find approximate days until expiry (0-730 days range)
|
||||
local low=0 high=730 mid seconds
|
||||
while [ $low -lt $high ]; do
|
||||
mid=$(( (low + high + 1) / 2 ))
|
||||
seconds=$((mid * 86400))
|
||||
if openssl x509 -checkend "$seconds" -noout -in "$cert_file" >/dev/null 2>&1; then
|
||||
low=$mid
|
||||
else
|
||||
high=$((mid - 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$low"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Helper: HTTP health check - returns status code
|
||||
check_http_health() {
|
||||
local url="$1"
|
||||
local timeout="${2:-5}"
|
||||
local status_code=""
|
||||
|
||||
# Use wget to get HTTP status code (available on OpenWrt)
|
||||
# wget -S prints headers, we extract the status code
|
||||
status_code=$(wget --spider -S -T "$timeout" -t 1 "$url" 2>&1 | \
|
||||
grep "HTTP/" | tail -1 | awk '{print $2}')
|
||||
|
||||
# If wget spider failed, try with -O /dev/null
|
||||
if [ -z "$status_code" ]; then
|
||||
status_code=$(wget -q -O /dev/null -S -T "$timeout" -t 1 "$url" 2>&1 | \
|
||||
grep "HTTP/" | tail -1 | awk '{print $2}')
|
||||
fi
|
||||
|
||||
echo "${status_code:-0}"
|
||||
}
|
||||
|
||||
# Helper: Check backend (local uhttpd) and frontend (HAProxy) health
|
||||
check_site_http_health() {
|
||||
local port="$1"
|
||||
local domain="$2"
|
||||
local ssl="$3"
|
||||
local backend_code="" frontend_code=""
|
||||
|
||||
# Check backend (local uhttpd instance)
|
||||
if [ -n "$port" ]; then
|
||||
backend_code=$(check_http_health "http://127.0.0.1:${port}/" 3)
|
||||
fi
|
||||
|
||||
# Check frontend (through HAProxy)
|
||||
if [ -n "$domain" ]; then
|
||||
local protocol="http"
|
||||
[ "$ssl" = "1" ] && protocol="https"
|
||||
frontend_code=$(check_http_health "${protocol}://${domain}/" 5)
|
||||
fi
|
||||
|
||||
echo "${backend_code:-0}:${frontend_code:-0}"
|
||||
}
|
||||
|
||||
# Get hosting status for all sites with DNS and cert health
|
||||
@ -909,6 +982,46 @@ _add_site_health() {
|
||||
json_add_string "cert_status" "none"
|
||||
fi
|
||||
|
||||
# HTTP health check (backend and frontend)
|
||||
if [ -n "$port" ] || [ -n "$domain" ]; then
|
||||
local http_result backend_code frontend_code
|
||||
http_result=$(check_site_http_health "$port" "$domain" "$ssl")
|
||||
backend_code="${http_result%%:*}"
|
||||
frontend_code="${http_result##*:}"
|
||||
|
||||
# Backend status (local uhttpd)
|
||||
if [ -n "$port" ]; then
|
||||
json_add_int "http_backend" "$backend_code"
|
||||
if [ "$backend_code" = "200" ]; then
|
||||
json_add_string "backend_status" "ok"
|
||||
elif [ "$backend_code" = "0" ]; then
|
||||
json_add_string "backend_status" "down"
|
||||
else
|
||||
json_add_string "backend_status" "error"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Frontend status (HAProxy)
|
||||
if [ -n "$domain" ]; then
|
||||
json_add_int "http_frontend" "$frontend_code"
|
||||
if [ "$frontend_code" = "200" ]; then
|
||||
json_add_string "frontend_status" "ok"
|
||||
elif [ "$frontend_code" = "0" ]; then
|
||||
json_add_string "frontend_status" "down"
|
||||
elif [ "$frontend_code" = "503" ]; then
|
||||
json_add_string "frontend_status" "unavailable"
|
||||
elif [ "$frontend_code" -ge 500 ] 2>/dev/null; then
|
||||
json_add_string "frontend_status" "error"
|
||||
elif [ "$frontend_code" -ge 400 ] 2>/dev/null; then
|
||||
json_add_string "frontend_status" "client_error"
|
||||
elif [ "$frontend_code" -ge 300 ] 2>/dev/null; then
|
||||
json_add_string "frontend_status" "redirect"
|
||||
else
|
||||
json_add_string "frontend_status" "unknown"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Publish status
|
||||
local publish_status="draft"
|
||||
if [ "$enabled" = "1" ] && [ "$has_content" = "1" ]; then
|
||||
@ -942,10 +1055,11 @@ method_check_site_health() {
|
||||
return
|
||||
fi
|
||||
|
||||
local name domain ssl
|
||||
local name domain ssl port
|
||||
name=$(get_uci "$id" name "")
|
||||
domain=$(get_uci "$id" domain "")
|
||||
ssl=$(get_uci "$id" ssl "1")
|
||||
port=$(get_uci "$id" port "")
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
json_init
|
||||
@ -1048,6 +1162,65 @@ method_check_site_health() {
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
# HTTP health check (backend and frontend)
|
||||
json_add_object "http"
|
||||
if [ -n "$port" ] || [ -n "$domain" ]; then
|
||||
local http_result backend_code frontend_code
|
||||
http_result=$(check_site_http_health "$port" "$domain" "$ssl")
|
||||
backend_code="${http_result%%:*}"
|
||||
frontend_code="${http_result##*:}"
|
||||
|
||||
# Backend (local uhttpd)
|
||||
if [ -n "$port" ]; then
|
||||
json_add_object "backend"
|
||||
json_add_int "code" "$backend_code"
|
||||
json_add_string "url" "http://127.0.0.1:${port}/"
|
||||
if [ "$backend_code" = "200" ]; then
|
||||
json_add_string "status" "ok"
|
||||
elif [ "$backend_code" = "0" ]; then
|
||||
json_add_string "status" "down"
|
||||
json_add_string "message" "Connection failed"
|
||||
else
|
||||
json_add_string "status" "error"
|
||||
json_add_string "message" "HTTP $backend_code"
|
||||
fi
|
||||
json_close_object
|
||||
fi
|
||||
|
||||
# Frontend (through HAProxy)
|
||||
if [ -n "$domain" ]; then
|
||||
local protocol="http"
|
||||
[ "$ssl" = "1" ] && protocol="https"
|
||||
json_add_object "frontend"
|
||||
json_add_int "code" "$frontend_code"
|
||||
json_add_string "url" "${protocol}://${domain}/"
|
||||
if [ "$frontend_code" = "200" ]; then
|
||||
json_add_string "status" "ok"
|
||||
elif [ "$frontend_code" = "0" ]; then
|
||||
json_add_string "status" "down"
|
||||
json_add_string "message" "Connection failed"
|
||||
elif [ "$frontend_code" = "503" ]; then
|
||||
json_add_string "status" "unavailable"
|
||||
json_add_string "message" "Service unavailable (backend down)"
|
||||
elif [ "$frontend_code" -ge 500 ] 2>/dev/null; then
|
||||
json_add_string "status" "error"
|
||||
json_add_string "message" "Server error $frontend_code"
|
||||
elif [ "$frontend_code" -ge 400 ] 2>/dev/null; then
|
||||
json_add_string "status" "client_error"
|
||||
json_add_string "message" "HTTP $frontend_code"
|
||||
elif [ "$frontend_code" -ge 300 ] 2>/dev/null; then
|
||||
json_add_string "status" "redirect"
|
||||
json_add_string "message" "Redirecting ($frontend_code)"
|
||||
else
|
||||
json_add_string "status" "unknown"
|
||||
fi
|
||||
json_close_object
|
||||
fi
|
||||
else
|
||||
json_add_string "status" "no_endpoints"
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ LUCI_DESCRIPTION:=Unified entry point for all SecuBox applications with tabbed n
|
||||
LUCI_DEPENDS:=+luci-base +luci-theme-secubox
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=0.7.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
||||
|
||||
|
||||
@ -40,6 +40,17 @@ var callDashboardData = rpc.declare({
|
||||
expect: { counts: {} }
|
||||
});
|
||||
|
||||
var callGetProxyMode = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'get_proxy_mode'
|
||||
});
|
||||
|
||||
var callSetProxyMode = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'set_proxy_mode',
|
||||
params: ['mode']
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
currentSection: 'dashboard',
|
||||
appStatuses: {},
|
||||
@ -55,7 +66,8 @@ return view.extend({
|
||||
portal.checkInstalledApps(),
|
||||
callGetServices().catch(function() { return []; }),
|
||||
callSecurityStats().catch(function() { return null; }),
|
||||
callDashboardData().catch(function() { return { counts: {} }; })
|
||||
callDashboardData().catch(function() { return { counts: {} }; }),
|
||||
callGetProxyMode().catch(function() { return { mode: 'direct' }; })
|
||||
]).then(function(results) {
|
||||
// Store installed apps info from the last promise
|
||||
self.installedApps = results[4] || {};
|
||||
@ -66,6 +78,8 @@ return view.extend({
|
||||
self.detectedServices = Array.isArray(svcResult) ? svcResult : (svcResult.services || []);
|
||||
// Security stats
|
||||
self.securityStats = results[6] || {};
|
||||
// Proxy mode
|
||||
self.proxyMode = results[8] || { mode: 'direct' };
|
||||
return results;
|
||||
});
|
||||
},
|
||||
@ -324,6 +338,12 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// Proxy Mode Switcher
|
||||
this.renderProxyModeSwitcher(),
|
||||
|
||||
// Speed Test Widget
|
||||
this.renderSpeedTestWidget(),
|
||||
|
||||
// Featured Apps
|
||||
E('h3', { 'style': 'margin: 1.5rem 0 1rem; color: var(--cyber-text-primary);' }, 'Quick Access'),
|
||||
E('div', { 'class': 'sb-app-grid' },
|
||||
@ -631,6 +651,345 @@ return view.extend({
|
||||
return bytes.toFixed(1) + ' ' + units[i];
|
||||
},
|
||||
|
||||
renderProxyModeSwitcher: function() {
|
||||
var self = this;
|
||||
var currentMode = (this.proxyMode && this.proxyMode.mode) || 'direct';
|
||||
|
||||
var modes = [
|
||||
{ id: 'direct', name: 'Direct', icon: '🌐', desc: 'No proxy - direct internet access' },
|
||||
{ id: 'cdn', name: 'CDN Cache', icon: '⚡', desc: 'HTTP caching via local proxy (port 3128)' },
|
||||
{ id: 'tor', name: 'Tor', icon: '🧅', desc: 'Route traffic through Tor network' },
|
||||
{ id: 'mitmproxy', name: 'MITM', icon: '🔍', desc: 'Traffic inspection via mitmproxy' }
|
||||
];
|
||||
|
||||
// Quick access buttons based on current mode
|
||||
var quickLinks = {
|
||||
direct: [
|
||||
{ icon: '🔧', label: 'Network', path: 'admin/network/network' },
|
||||
{ icon: '🛡️', label: 'Firewall', path: 'admin/network/firewall' },
|
||||
{ icon: '📡', label: 'DHCP/DNS', path: 'admin/network/dhcp' }
|
||||
],
|
||||
cdn: [
|
||||
{ icon: '⚡', label: 'CDN Cache', path: 'admin/services/cdn-cache' },
|
||||
{ icon: '📊', label: 'Statistics', path: 'admin/services/cdn-cache/statistics' },
|
||||
{ icon: '📄', label: 'View PAC', path: null, external: '/wpad/wpad.dat' }
|
||||
],
|
||||
tor: [
|
||||
{ icon: '🧅', label: 'Tor Shield', path: 'admin/services/tor-shield' },
|
||||
{ icon: '🔒', label: 'Hidden Services', path: 'admin/services/tor-shield/hidden' },
|
||||
{ icon: '📊', label: 'Tor Status', path: 'admin/services/tor-shield' }
|
||||
],
|
||||
mitmproxy: [
|
||||
{ icon: '🔍', label: 'mitmproxy', path: 'admin/services/mitmproxy' },
|
||||
{ icon: '🌐', label: 'Web UI', path: null, external: 'http://192.168.255.1:8080' },
|
||||
{ icon: '📜', label: 'Get CA Cert', path: null, external: 'http://mitm.it' }
|
||||
]
|
||||
};
|
||||
|
||||
var currentLinks = quickLinks[currentMode] || quickLinks.direct;
|
||||
|
||||
var btnStyle = 'display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0.75rem; border-radius: 6px; border: 1px solid var(--cyber-border, #444); background: var(--cyber-bg-tertiary, #16213e); color: var(--cyber-text-primary, #fff); font-size: 0.8em; text-decoration: none; cursor: pointer; transition: all 0.2s;';
|
||||
|
||||
return E('div', { 'class': 'sb-proxy-switcher', 'style': 'margin: 1.5rem 0; padding: 1rem; background: var(--cyber-bg-secondary, #1a1a2e); border-radius: 12px; border: 1px solid var(--cyber-border, #333);' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;' }, [
|
||||
E('span', { 'style': 'font-size: 1.2em;' }, '🔀'),
|
||||
E('h4', { 'style': 'margin: 0; color: var(--cyber-text-primary, #fff);' }, 'Network Proxy Mode'),
|
||||
E('span', { 'style': 'margin-left: auto; font-size: 0.8em; color: var(--cyber-text-secondary, #888);' }, 'WPAD auto-config enabled')
|
||||
]),
|
||||
E('div', { 'class': 'sb-proxy-modes', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem;' },
|
||||
modes.map(function(mode) {
|
||||
var isActive = currentMode === mode.id;
|
||||
return E('button', {
|
||||
'class': 'sb-proxy-mode-btn' + (isActive ? ' active' : ''),
|
||||
'data-mode': mode.id,
|
||||
'style': 'display: flex; flex-direction: column; align-items: center; padding: 0.75rem; border-radius: 8px; border: 2px solid ' + (isActive ? 'var(--cyber-accent, #0ff)' : 'var(--cyber-border, #333)') + '; background: ' + (isActive ? 'rgba(0, 255, 255, 0.1)' : 'var(--cyber-bg-tertiary, #16213e)') + '; cursor: pointer; transition: all 0.2s;',
|
||||
'click': function() { self.handleProxyModeChange(mode.id); }
|
||||
}, [
|
||||
E('span', { 'style': 'font-size: 1.5em; margin-bottom: 0.25rem;' }, mode.icon),
|
||||
E('span', { 'style': 'font-weight: 600; color: ' + (isActive ? 'var(--cyber-accent, #0ff)' : 'var(--cyber-text-primary, #fff)') + ';' }, mode.name),
|
||||
E('span', { 'style': 'font-size: 0.7em; color: var(--cyber-text-secondary, #888); text-align: center; margin-top: 0.25rem;' }, mode.desc)
|
||||
]);
|
||||
})
|
||||
),
|
||||
// Quick Access Buttons
|
||||
E('div', { 'class': 'sb-proxy-quick-access', 'style': 'display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--cyber-border, #333);' }, [
|
||||
E('span', { 'style': 'font-size: 0.75em; color: var(--cyber-text-secondary, #888); margin-right: 0.5rem; align-self: center;' }, 'Quick Access:')
|
||||
].concat(currentLinks.map(function(link) {
|
||||
if (link.external) {
|
||||
return E('a', {
|
||||
'href': link.external,
|
||||
'target': '_blank',
|
||||
'style': btnStyle
|
||||
}, [
|
||||
E('span', {}, link.icon),
|
||||
E('span', {}, link.label)
|
||||
]);
|
||||
} else if (link.path) {
|
||||
return E('a', {
|
||||
'href': L.url(link.path),
|
||||
'style': btnStyle
|
||||
}, [
|
||||
E('span', {}, link.icon),
|
||||
E('span', {}, link.label)
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean)))
|
||||
]);
|
||||
},
|
||||
|
||||
renderSpeedTestWidget: function() {
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'sb-speedtest-widget', 'style': 'margin: 1.5rem 0; padding: 1.25rem; background: var(--cyber-bg-secondary, #1a1a2e); border-radius: 12px; border: 1px solid var(--cyber-border, #333);' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;' }, [
|
||||
E('span', { 'style': 'font-size: 1.2em;' }, '🚀'),
|
||||
E('h4', { 'style': 'margin: 0; color: var(--cyber-text-primary, #fff);' }, 'Speed Test'),
|
||||
E('span', { 'style': 'margin-left: auto; font-size: 0.75em; color: var(--cyber-text-secondary, #888);' }, 'Test your connection speed')
|
||||
]),
|
||||
E('div', { 'class': 'sb-speedtest-results', 'style': 'display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem;' }, [
|
||||
E('div', { 'class': 'sb-speedtest-metric', 'style': 'text-align: center; padding: 1rem; background: var(--cyber-bg-tertiary, #16213e); border-radius: 8px;' }, [
|
||||
E('div', { 'style': 'font-size: 0.75em; color: var(--cyber-text-secondary, #888); margin-bottom: 0.25rem;' }, '⬇️ Download'),
|
||||
E('div', { 'id': 'speedtest-download', 'style': 'font-size: 1.5em; font-weight: bold; color: #10b981;' }, '-- Mbps'),
|
||||
E('div', { 'id': 'speedtest-download-progress', 'style': 'height: 4px; background: #333; border-radius: 2px; margin-top: 0.5rem; overflow: hidden;' }, [
|
||||
E('div', { 'style': 'height: 100%; width: 0%; background: linear-gradient(90deg, #10b981, #0ff); transition: width 0.3s;' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sb-speedtest-metric', 'style': 'text-align: center; padding: 1rem; background: var(--cyber-bg-tertiary, #16213e); border-radius: 8px;' }, [
|
||||
E('div', { 'style': 'font-size: 0.75em; color: var(--cyber-text-secondary, #888); margin-bottom: 0.25rem;' }, '⬆️ Upload'),
|
||||
E('div', { 'id': 'speedtest-upload', 'style': 'font-size: 1.5em; font-weight: bold; color: #8b5cf6;' }, '-- Mbps'),
|
||||
E('div', { 'id': 'speedtest-upload-progress', 'style': 'height: 4px; background: #333; border-radius: 2px; margin-top: 0.5rem; overflow: hidden;' }, [
|
||||
E('div', { 'style': 'height: 100%; width: 0%; background: linear-gradient(90deg, #8b5cf6, #ec4899); transition: width 0.3s;' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sb-speedtest-metric', 'style': 'text-align: center; padding: 1rem; background: var(--cyber-bg-tertiary, #16213e); border-radius: 8px;' }, [
|
||||
E('div', { 'style': 'font-size: 0.75em; color: var(--cyber-text-secondary, #888); margin-bottom: 0.25rem;' }, '📶 Ping'),
|
||||
E('div', { 'id': 'speedtest-ping', 'style': 'font-size: 1.5em; font-weight: bold; color: #f59e0b;' }, '-- ms'),
|
||||
E('div', { 'id': 'speedtest-jitter', 'style': 'font-size: 0.7em; color: var(--cyber-text-secondary, #888); margin-top: 0.25rem;' }, 'Jitter: -- ms')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 1rem;' }, [
|
||||
E('button', {
|
||||
'id': 'speedtest-btn',
|
||||
'class': 'sb-speedtest-btn',
|
||||
'style': 'flex: 1; padding: 0.75rem 1.5rem; border-radius: 8px; border: none; background: linear-gradient(135deg, #0ff, #00a0a0); color: #000; font-weight: 600; cursor: pointer; transition: all 0.2s; font-size: 1em;',
|
||||
'click': function(ev) { self.runSpeedTest(ev.target); }
|
||||
}, '▶️ Start Speed Test'),
|
||||
E('select', {
|
||||
'id': 'speedtest-server',
|
||||
'style': 'padding: 0.75rem; border-radius: 8px; border: 1px solid var(--cyber-border, #333); background: var(--cyber-bg-tertiary, #16213e); color: var(--cyber-text-primary, #fff);'
|
||||
}, [
|
||||
E('option', { 'value': 'cloudflare' }, 'Cloudflare'),
|
||||
E('option', { 'value': 'fast' }, 'Fast.com (Netflix)'),
|
||||
E('option', { 'value': 'local' }, 'Local (Router)')
|
||||
])
|
||||
]),
|
||||
E('div', { 'id': 'speedtest-status', 'style': 'margin-top: 0.75rem; font-size: 0.8em; color: var(--cyber-text-secondary, #888); text-align: center;' }, 'Ready to test')
|
||||
]);
|
||||
},
|
||||
|
||||
runSpeedTest: function(btn) {
|
||||
var self = this;
|
||||
var server = document.getElementById('speedtest-server').value;
|
||||
var statusEl = document.getElementById('speedtest-status');
|
||||
var downloadEl = document.getElementById('speedtest-download');
|
||||
var uploadEl = document.getElementById('speedtest-upload');
|
||||
var pingEl = document.getElementById('speedtest-ping');
|
||||
var jitterEl = document.getElementById('speedtest-jitter');
|
||||
var dlProgress = document.querySelector('#speedtest-download-progress > div');
|
||||
var ulProgress = document.querySelector('#speedtest-upload-progress > div');
|
||||
|
||||
// Disable button during test
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Testing...';
|
||||
btn.style.opacity = '0.7';
|
||||
|
||||
// Reset values
|
||||
downloadEl.textContent = '-- Mbps';
|
||||
uploadEl.textContent = '-- Mbps';
|
||||
pingEl.textContent = '-- ms';
|
||||
jitterEl.textContent = 'Jitter: -- ms';
|
||||
dlProgress.style.width = '0%';
|
||||
ulProgress.style.width = '0%';
|
||||
|
||||
// Test endpoints based on server selection
|
||||
var testUrls = {
|
||||
cloudflare: {
|
||||
download: 'https://speed.cloudflare.com/__down?bytes=10000000',
|
||||
upload: 'https://speed.cloudflare.com/__up',
|
||||
ping: 'https://speed.cloudflare.com/__down?bytes=0'
|
||||
},
|
||||
fast: {
|
||||
download: 'https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=1',
|
||||
ping: 'https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=1'
|
||||
},
|
||||
local: {
|
||||
download: '/cgi-bin/luci/admin/status/realtime/bandwidth_status',
|
||||
ping: '/cgi-bin/luci/'
|
||||
}
|
||||
};
|
||||
|
||||
var urls = testUrls[server] || testUrls.cloudflare;
|
||||
|
||||
// Measure ping first
|
||||
statusEl.textContent = '📡 Measuring latency...';
|
||||
var pingStart = performance.now();
|
||||
var pings = [];
|
||||
|
||||
var measurePing = function(attempts) {
|
||||
if (attempts <= 0) {
|
||||
var avgPing = pings.reduce(function(a, b) { return a + b; }, 0) / pings.length;
|
||||
var jitter = Math.max.apply(null, pings) - Math.min.apply(null, pings);
|
||||
pingEl.textContent = avgPing.toFixed(0) + ' ms';
|
||||
jitterEl.textContent = 'Jitter: ' + jitter.toFixed(0) + ' ms';
|
||||
runDownloadTest();
|
||||
return;
|
||||
}
|
||||
|
||||
var start = performance.now();
|
||||
fetch(urls.ping, { method: 'HEAD', cache: 'no-store', mode: 'no-cors' })
|
||||
.then(function() {
|
||||
pings.push(performance.now() - start);
|
||||
measurePing(attempts - 1);
|
||||
})
|
||||
.catch(function() {
|
||||
pings.push(performance.now() - start);
|
||||
measurePing(attempts - 1);
|
||||
});
|
||||
};
|
||||
|
||||
var runDownloadTest = function() {
|
||||
statusEl.textContent = '⬇️ Testing download speed...';
|
||||
|
||||
if (server === 'fast') {
|
||||
// Fast.com requires API call first
|
||||
downloadEl.textContent = 'N/A';
|
||||
dlProgress.style.width = '100%';
|
||||
runUploadTest();
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadStart = performance.now();
|
||||
var receivedBytes = 0;
|
||||
var downloadSize = server === 'local' ? 100000 : 10000000; // 10MB for cloudflare
|
||||
|
||||
fetch(urls.download, { cache: 'no-store' })
|
||||
.then(function(response) {
|
||||
var reader = response.body.getReader();
|
||||
var read = function() {
|
||||
return reader.read().then(function(result) {
|
||||
if (result.done) {
|
||||
var duration = (performance.now() - downloadStart) / 1000;
|
||||
var speedBps = (receivedBytes * 8) / duration;
|
||||
var speedMbps = speedBps / 1000000;
|
||||
downloadEl.textContent = speedMbps.toFixed(2) + ' Mbps';
|
||||
dlProgress.style.width = '100%';
|
||||
runUploadTest();
|
||||
return;
|
||||
}
|
||||
receivedBytes += result.value.length;
|
||||
var progress = Math.min((receivedBytes / downloadSize) * 100, 100);
|
||||
dlProgress.style.width = progress + '%';
|
||||
return read();
|
||||
});
|
||||
};
|
||||
return read();
|
||||
})
|
||||
.catch(function(err) {
|
||||
downloadEl.textContent = 'Error';
|
||||
dlProgress.style.width = '100%';
|
||||
runUploadTest();
|
||||
});
|
||||
};
|
||||
|
||||
var runUploadTest = function() {
|
||||
statusEl.textContent = '⬆️ Testing upload speed...';
|
||||
|
||||
if (server !== 'cloudflare') {
|
||||
uploadEl.textContent = 'N/A';
|
||||
ulProgress.style.width = '100%';
|
||||
finishTest();
|
||||
return;
|
||||
}
|
||||
|
||||
var uploadSize = 2000000; // 2MB
|
||||
var uploadData = new Uint8Array(uploadSize);
|
||||
var uploadStart = performance.now();
|
||||
|
||||
fetch(urls.upload, {
|
||||
method: 'POST',
|
||||
body: uploadData,
|
||||
cache: 'no-store'
|
||||
})
|
||||
.then(function() {
|
||||
var duration = (performance.now() - uploadStart) / 1000;
|
||||
var speedBps = (uploadSize * 8) / duration;
|
||||
var speedMbps = speedBps / 1000000;
|
||||
uploadEl.textContent = speedMbps.toFixed(2) + ' Mbps';
|
||||
ulProgress.style.width = '100%';
|
||||
finishTest();
|
||||
})
|
||||
.catch(function(err) {
|
||||
uploadEl.textContent = 'Error';
|
||||
ulProgress.style.width = '100%';
|
||||
finishTest();
|
||||
});
|
||||
};
|
||||
|
||||
var finishTest = function() {
|
||||
statusEl.textContent = '✅ Test complete - ' + new Date().toLocaleTimeString();
|
||||
btn.disabled = false;
|
||||
btn.textContent = '▶️ Start Speed Test';
|
||||
btn.style.opacity = '1';
|
||||
};
|
||||
|
||||
// Start with ping measurement (5 attempts)
|
||||
measurePing(5);
|
||||
},
|
||||
|
||||
handleProxyModeChange: function(mode) {
|
||||
var self = this;
|
||||
var buttons = document.querySelectorAll('.sb-proxy-mode-btn');
|
||||
|
||||
// Visual feedback - disable all buttons
|
||||
buttons.forEach(function(btn) {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.6';
|
||||
});
|
||||
|
||||
return callSetProxyMode(mode).then(function(result) {
|
||||
if (result && result.success) {
|
||||
// Update UI
|
||||
self.proxyMode = { mode: mode };
|
||||
buttons.forEach(function(btn) {
|
||||
var isActive = btn.dataset.mode === mode;
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.style.borderColor = isActive ? 'var(--cyber-accent, #0ff)' : 'var(--cyber-border, #333)';
|
||||
btn.style.background = isActive ? 'rgba(0, 255, 255, 0.1)' : 'var(--cyber-bg-tertiary, #16213e)';
|
||||
var nameSpan = btn.querySelector('span:nth-child(2)');
|
||||
if (nameSpan) {
|
||||
nameSpan.style.color = isActive ? 'var(--cyber-accent, #0ff)' : 'var(--cyber-text-primary, #fff)';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
ui.addNotification(null, E('p', {}, 'Proxy mode changed to: ' + mode), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, 'Failed to change proxy mode: ' + (result.error || 'unknown error')), 'error');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, 'Error changing proxy mode: ' + err.message), 'error');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-secubox-security-threats
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
@ -275,16 +275,28 @@ get_security_stats() {
|
||||
local haproxy_conns=0
|
||||
local invalid_conns=0
|
||||
|
||||
# WAN dropped packets (from kernel stats)
|
||||
if [ -f /sys/class/net/br-wan/statistics/rx_dropped ]; then
|
||||
wan_drops=$(cat /sys/class/net/br-wan/statistics/rx_dropped 2>/dev/null)
|
||||
elif [ -f /sys/class/net/eth1/statistics/rx_dropped ]; then
|
||||
wan_drops=$(cat /sys/class/net/eth1/statistics/rx_dropped 2>/dev/null)
|
||||
# Get actual WAN interface from UCI
|
||||
local wan_iface=$(uci -q get network.wan.device || uci -q get network.wan.ifname)
|
||||
[ -z "$wan_iface" ] && wan_iface="eth0"
|
||||
|
||||
# WAN dropped packets from nftables (actual firewall drops on input chain)
|
||||
# Count packets dropped/rejected on wan zone input
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
# Get drop counters from firewall input chain for wan
|
||||
wan_drops=$(nft list chain inet fw4 input 2>/dev/null | grep -E "iifname.*$wan_iface.*drop|iifname.*$wan_iface.*reject" | grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
|
||||
# Also count from forward chain drops (wan to lan blocked)
|
||||
local wan_fwd_drops=$(nft list chain inet fw4 forward 2>/dev/null | grep -E "iifname.*$wan_iface.*drop|iifname.*$wan_iface.*reject" | grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
|
||||
wan_drops=$((${wan_drops:-0} + ${wan_fwd_drops:-0}))
|
||||
fi
|
||||
wan_drops=${wan_drops:-0}
|
||||
|
||||
# Firewall rejects from logs (last 24h)
|
||||
fw_rejects=$(logread 2>/dev/null | grep -c "reject\|drop" || echo 0)
|
||||
# Firewall rejects - count from nftables counters (more accurate than logs)
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
fw_rejects=$(nft list ruleset 2>/dev/null | grep -E "reject|drop" | grep -oE 'packets [0-9]+' | awk '{sum+=$2} END {print sum+0}')
|
||||
else
|
||||
# Fallback to log parsing
|
||||
fw_rejects=$(logread 2>/dev/null | grep -c "reject\|DROP\|REJECT" || echo 0)
|
||||
fi
|
||||
fw_rejects=$(echo "$fw_rejects" | tr -d '\n')
|
||||
fw_rejects=${fw_rejects:-0}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-secubox
|
||||
PKG_VERSION:=0.7.1
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
|
||||
@ -35,7 +35,9 @@
|
||||
"get_public_ips",
|
||||
"get_network_health",
|
||||
"get_vital_services",
|
||||
"get_full_health_report"
|
||||
"get_full_health_report",
|
||||
"get_services",
|
||||
"get_proxy_mode"
|
||||
],
|
||||
"uci": [
|
||||
"get",
|
||||
@ -71,7 +73,8 @@
|
||||
"apply_profile",
|
||||
"rollback_profile",
|
||||
"install_appstore_app",
|
||||
"remove_appstore_app"
|
||||
"remove_appstore_app",
|
||||
"set_proxy_mode"
|
||||
],
|
||||
"uci": [
|
||||
"set",
|
||||
|
||||
@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-streamlit
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
@ -424,6 +424,97 @@
|
||||
color: #0ff;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Instances Table
|
||||
============================================ */
|
||||
|
||||
.st-instances-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.st-instances-table th,
|
||||
.st-instances-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.st-instances-table th {
|
||||
color: #0ff;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1px;
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.st-instances-table td {
|
||||
color: #f1f5f9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.st-instances-table tr:hover td {
|
||||
background: rgba(0, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.st-instances-table tr.st-row-active td {
|
||||
background: rgba(0, 255, 255, 0.1);
|
||||
border-left: 3px solid #0ff;
|
||||
}
|
||||
|
||||
.st-instances-table .st-mono {
|
||||
font-family: "Monaco", "Consolas", monospace;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.st-instances-table a {
|
||||
color: #0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.st-instances-table a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.st-app-desc {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.st-status-dot {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.st-status-dot.st-running {
|
||||
text-shadow: 0 0 8px #10b981;
|
||||
}
|
||||
|
||||
.st-status-dot.st-stopped {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.st-no-padding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.st-link {
|
||||
color: #0ff;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.st-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.st-btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Empty State
|
||||
============================================ */
|
||||
|
||||
@ -122,7 +122,7 @@ return view.extend({
|
||||
return E('div', { 'class': 'st-main-grid' }, [
|
||||
this.renderControlCard(),
|
||||
this.renderInfoCard(),
|
||||
this.renderLogsCard()
|
||||
this.renderInstancesCard()
|
||||
]);
|
||||
},
|
||||
|
||||
@ -236,26 +236,69 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
renderLogsCard: function() {
|
||||
var logs = this.logsData || [];
|
||||
renderInstancesCard: function() {
|
||||
var apps = this.appsData || {};
|
||||
var instances = apps.apps || [];
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'st-card st-card-full' }, [
|
||||
E('div', { 'class': 'st-card-header' }, [
|
||||
E('div', { 'class': 'st-card-title' }, [
|
||||
E('span', {}, '\uD83D\uDCDC'),
|
||||
' ' + _('Recent Logs')
|
||||
])
|
||||
E('span', {}, '\uD83D\uDCCA'),
|
||||
' ' + _('Instances')
|
||||
]),
|
||||
E('a', {
|
||||
'href': L.url('admin', 'services', 'streamlit', 'apps'),
|
||||
'class': 'st-link'
|
||||
}, _('Manage Apps') + ' \u2192')
|
||||
]),
|
||||
E('div', { 'class': 'st-card-body' }, [
|
||||
logs.length > 0 ?
|
||||
E('div', { 'class': 'st-logs', 'id': 'st-logs' },
|
||||
logs.slice(-20).map(function(line) {
|
||||
return E('div', { 'class': 'st-logs-line' }, line);
|
||||
})
|
||||
) :
|
||||
E('div', { 'class': 'st-card-body st-no-padding' }, [
|
||||
instances.length > 0 ?
|
||||
E('table', { 'class': 'st-instances-table', 'id': 'st-instances' }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, _('App')),
|
||||
E('th', {}, _('Port')),
|
||||
E('th', {}, _('Status')),
|
||||
E('th', {}, _('Published')),
|
||||
E('th', {}, _('Domain'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {},
|
||||
instances.map(function(app) {
|
||||
var isActive = app.active || (self.statusData && self.statusData.active_app === app.name);
|
||||
var isRunning = isActive && self.statusData && self.statusData.running;
|
||||
var statusIcon = isRunning ? '\uD83D\uDFE2' : '\uD83D\uDD34';
|
||||
var statusText = isRunning ? _('Running') : _('Stopped');
|
||||
var publishedIcon = app.published ? '\u2705' : '\u26AA';
|
||||
var domain = app.domain || (app.published ? app.name + '.example.com' : '-');
|
||||
|
||||
return E('tr', { 'class': isActive ? 'st-row-active' : '' }, [
|
||||
E('td', {}, [
|
||||
E('strong', {}, app.name || app.id),
|
||||
app.description ? E('div', { 'class': 'st-app-desc' }, app.description) : null
|
||||
]),
|
||||
E('td', { 'class': 'st-mono' }, String(app.port || 8501)),
|
||||
E('td', {}, [
|
||||
E('span', { 'class': 'st-status-dot ' + (isRunning ? 'st-running' : 'st-stopped') }, statusIcon),
|
||||
' ' + statusText
|
||||
]),
|
||||
E('td', {}, publishedIcon),
|
||||
E('td', {}, domain !== '-' ?
|
||||
E('a', { 'href': 'https://' + domain, 'target': '_blank' }, domain) :
|
||||
'-'
|
||||
)
|
||||
]);
|
||||
})
|
||||
)
|
||||
]) :
|
||||
E('div', { 'class': 'st-empty' }, [
|
||||
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCED'),
|
||||
E('div', {}, _('No logs available'))
|
||||
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'),
|
||||
E('div', {}, _('No apps deployed')),
|
||||
E('a', {
|
||||
'href': L.url('admin', 'services', 'streamlit', 'apps'),
|
||||
'class': 'st-btn st-btn-primary st-btn-sm'
|
||||
}, _('Deploy First App'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
@ -286,12 +329,26 @@ return view.extend({
|
||||
statActive.textContent = status.active_app || 'hello';
|
||||
}
|
||||
|
||||
// Update logs
|
||||
var logsContainer = document.getElementById('st-logs');
|
||||
if (logsContainer && this.logsData) {
|
||||
logsContainer.innerHTML = '';
|
||||
this.logsData.slice(-20).forEach(function(line) {
|
||||
logsContainer.appendChild(E('div', { 'class': 'st-logs-line' }, line));
|
||||
// Update instances table status indicators
|
||||
var instancesTable = document.getElementById('st-instances');
|
||||
if (instancesTable) {
|
||||
var apps = this.appsData && this.appsData.apps || [];
|
||||
var rows = instancesTable.querySelectorAll('tbody tr');
|
||||
rows.forEach(function(row, idx) {
|
||||
if (apps[idx]) {
|
||||
var app = apps[idx];
|
||||
var isActive = app.active || (self.statusData && self.statusData.active_app === app.name);
|
||||
var isRunning = isActive && self.statusData && self.statusData.running;
|
||||
row.className = isActive ? 'st-row-active' : '';
|
||||
var statusCell = row.querySelector('td:nth-child(3)');
|
||||
if (statusCell) {
|
||||
statusCell.innerHTML = '';
|
||||
var statusIcon = isRunning ? '\uD83D\uDFE2' : '\uD83D\uDD34';
|
||||
var statusText = isRunning ? _('Running') : _('Stopped');
|
||||
statusCell.appendChild(E('span', { 'class': 'st-status-dot ' + (isRunning ? 'st-running' : 'st-stopped') }, statusIcon));
|
||||
statusCell.appendChild(document.createTextNode(' ' + statusText));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-tor-shield
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
@ -622,6 +622,13 @@ set_bridges() {
|
||||
# Now initialize output JSON
|
||||
json_init
|
||||
|
||||
# Ensure bridges section exists
|
||||
uci -q get tor-shield.bridges >/dev/null 2>&1 || {
|
||||
uci set tor-shield.bridges=bridges
|
||||
uci set tor-shield.bridges.enabled='0'
|
||||
uci set tor-shield.bridges.type='obfs4'
|
||||
}
|
||||
|
||||
[ -n "$enabled" ] && uci set tor-shield.bridges.enabled="$enabled"
|
||||
[ -n "$type" ] && uci set tor-shield.bridges.type="$type"
|
||||
|
||||
@ -710,6 +717,28 @@ save_settings() {
|
||||
# Now initialize output JSON
|
||||
json_init
|
||||
|
||||
# Ensure required UCI sections exist before setting values
|
||||
uci -q get tor-shield.main >/dev/null 2>&1 || {
|
||||
uci set tor-shield.main=tor-shield
|
||||
uci set tor-shield.main.enabled='0'
|
||||
}
|
||||
uci -q get tor-shield.socks >/dev/null 2>&1 || {
|
||||
uci set tor-shield.socks=proxy
|
||||
uci set tor-shield.socks.port='9050'
|
||||
uci set tor-shield.socks.address='127.0.0.1'
|
||||
}
|
||||
uci -q get tor-shield.trans >/dev/null 2>&1 || {
|
||||
uci set tor-shield.trans=transparent
|
||||
uci set tor-shield.trans.port='9040'
|
||||
uci set tor-shield.trans.dns_port='9053'
|
||||
}
|
||||
uci -q get tor-shield.security >/dev/null 2>&1 || {
|
||||
uci set tor-shield.security=security
|
||||
uci set tor-shield.security.exit_nodes=''
|
||||
uci set tor-shield.security.exclude_exit_nodes=''
|
||||
uci set tor-shield.security.strict_nodes='0'
|
||||
}
|
||||
|
||||
# Apply settings
|
||||
[ -n "$mode" ] && uci set tor-shield.main.mode="$mode"
|
||||
[ -n "$dns_over_tor" ] && uci set tor-shield.main.dns_over_tor="$dns_over_tor"
|
||||
|
||||
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-haproxy
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=21
|
||||
PKG_RELEASE:=23
|
||||
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
@ -18,7 +18,7 @@ define Package/secubox-app-haproxy
|
||||
CATEGORY:=SecuBox
|
||||
SUBMENU:=Services
|
||||
TITLE:=HAProxy Load Balancer & Reverse Proxy
|
||||
DEPENDS:=+lxc +lxc-common +openssl-util +wget-ssl +tar +jsonfilter +acme +acme-acmesh +socat
|
||||
DEPENDS:=+lxc +lxc-common +openssl-util +wget-ssl +tar +jsonfilter +acme +acme-acmesh +socat +uhttpd
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
@ -73,6 +73,22 @@ endef
|
||||
define Package/secubox-app-haproxy/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
# Setup ACME challenge webserver (uhttpd instance on port 8402)
|
||||
ACME_WEBROOT="/var/www/acme-challenge"
|
||||
ACME_PORT="8402"
|
||||
mkdir -p "$$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
chmod -R 755 "$$ACME_WEBROOT"
|
||||
|
||||
# Configure uhttpd.acme if not exists
|
||||
if ! uci -q get uhttpd.acme >/dev/null 2>&1; then
|
||||
uci set uhttpd.acme=uhttpd
|
||||
uci set uhttpd.acme.listen_http="0.0.0.0:$$ACME_PORT"
|
||||
uci set uhttpd.acme.home="$$ACME_WEBROOT"
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd restart 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Sync existing ACME certificates on install
|
||||
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
||||
exit 0
|
||||
|
||||
@ -8,6 +8,30 @@ USE_PROCD=1
|
||||
|
||||
NAME="haproxy"
|
||||
PROG="/usr/sbin/haproxyctl"
|
||||
ACME_WEBROOT="/var/www/acme-challenge"
|
||||
ACME_PORT="8402"
|
||||
|
||||
# Setup ACME challenge webserver for certificate issuance
|
||||
# HAProxy routes /.well-known/acme-challenge/ to this server
|
||||
setup_acme_webserver() {
|
||||
# Create ACME challenge directory
|
||||
mkdir -p "$ACME_WEBROOT/.well-known/acme-challenge"
|
||||
chmod -R 755 "$ACME_WEBROOT"
|
||||
|
||||
# Configure uhttpd instance for ACME if not exists
|
||||
if ! uci -q get uhttpd.acme >/dev/null 2>&1; then
|
||||
uci set uhttpd.acme=uhttpd
|
||||
uci set uhttpd.acme.listen_http="0.0.0.0:$ACME_PORT"
|
||||
uci set uhttpd.acme.home="$ACME_WEBROOT"
|
||||
uci commit uhttpd
|
||||
/etc/init.d/uhttpd restart 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Ensure uhttpd is listening on ACME port
|
||||
if ! netstat -tln 2>/dev/null | grep -q ":$ACME_PORT "; then
|
||||
/etc/init.d/uhttpd restart 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
@ -16,6 +40,9 @@ start_service() {
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
# Ensure ACME challenge webserver is configured and running
|
||||
setup_acme_webserver
|
||||
|
||||
# Sync ACME certificates to HAProxy format before starting
|
||||
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-core
|
||||
PKG_VERSION:=0.10.0
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
PKG_ARCH:=all
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
PKG_MAINTAINER:=SecuBox Team
|
||||
|
||||
@ -237,6 +237,14 @@ case "$1" in
|
||||
json_add_object "get_services"
|
||||
json_close_object
|
||||
|
||||
# Proxy mode management
|
||||
json_add_object "get_proxy_mode"
|
||||
json_close_object
|
||||
|
||||
json_add_object "set_proxy_mode"
|
||||
json_add_string "mode" "string"
|
||||
json_close_object
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
@ -1658,6 +1666,128 @@ case "$1" in
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_proxy_mode)
|
||||
json_init
|
||||
local mode="direct"
|
||||
local wpad_enabled=0
|
||||
|
||||
# Check if WPAD PAC file exists and determine mode
|
||||
if [ -f "/www/wpad/wpad.dat" ]; then
|
||||
wpad_enabled=1
|
||||
if grep -q "SOCKS5.*9050" /www/wpad/wpad.dat 2>/dev/null; then
|
||||
mode="tor"
|
||||
elif grep -q "PROXY.*3128" /www/wpad/wpad.dat 2>/dev/null; then
|
||||
mode="cdn"
|
||||
elif grep -q "PROXY.*8080" /www/wpad/wpad.dat 2>/dev/null; then
|
||||
mode="mitmproxy"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check DHCP WPAD option
|
||||
local dhcp_wpad=$(uci -q get dhcp.lan.dhcp_option | grep -c "252")
|
||||
|
||||
json_add_string "mode" "$mode"
|
||||
json_add_boolean "wpad_enabled" "$wpad_enabled"
|
||||
json_add_boolean "dhcp_wpad" "$dhcp_wpad"
|
||||
json_add_string "pac_url" "http://192.168.255.1/wpad/wpad.dat"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
set_proxy_mode)
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var mode mode
|
||||
|
||||
json_init
|
||||
|
||||
mkdir -p /www/wpad
|
||||
|
||||
case "$mode" in
|
||||
direct)
|
||||
# Remove PAC file for direct mode
|
||||
rm -f /www/wpad/wpad.dat
|
||||
uci -q delete dhcp.lan.dhcp_option
|
||||
uci commit dhcp
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Proxy disabled - direct connections"
|
||||
;;
|
||||
cdn)
|
||||
# CDN cache mode - HTTP through nginx cache
|
||||
cat > /www/wpad/wpad.dat << 'PACEOF'
|
||||
function FindProxyForURL(url, host) {
|
||||
if (isPlainHostName(host) || shExpMatch(host, "*.local") || shExpMatch(host, "*.lan") ||
|
||||
isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
|
||||
isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
|
||||
isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") ||
|
||||
isInNet(dnsResolve(host), "127.0.0.0", "255.0.0.0")) {
|
||||
return "DIRECT";
|
||||
}
|
||||
if (url.substring(0, 5) == "http:") {
|
||||
return "PROXY 192.168.255.1:3128; DIRECT";
|
||||
}
|
||||
return "DIRECT";
|
||||
}
|
||||
PACEOF
|
||||
uci set dhcp.lan.dhcp_option="252,http://192.168.255.1/wpad/wpad.dat"
|
||||
uci commit dhcp
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "CDN cache mode enabled - HTTP cached"
|
||||
;;
|
||||
tor)
|
||||
# Tor bypass mode - HTTPS through Tor SOCKS
|
||||
cat > /www/wpad/wpad.dat << 'PACEOF'
|
||||
function FindProxyForURL(url, host) {
|
||||
if (isPlainHostName(host) || shExpMatch(host, "*.local") || shExpMatch(host, "*.lan") ||
|
||||
isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
|
||||
isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
|
||||
isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") ||
|
||||
isInNet(dnsResolve(host), "127.0.0.0", "255.0.0.0")) {
|
||||
return "DIRECT";
|
||||
}
|
||||
if (url.substring(0, 5) == "http:") {
|
||||
return "PROXY 192.168.255.1:3128; DIRECT";
|
||||
}
|
||||
if (url.substring(0, 6) == "https:") {
|
||||
return "SOCKS5 192.168.255.1:9050; DIRECT";
|
||||
}
|
||||
return "DIRECT";
|
||||
}
|
||||
PACEOF
|
||||
uci set dhcp.lan.dhcp_option="252,http://192.168.255.1/wpad/wpad.dat"
|
||||
uci commit dhcp
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Tor bypass mode enabled - HTTPS through Tor"
|
||||
;;
|
||||
mitmproxy)
|
||||
# mitmproxy mode - all traffic through mitmproxy
|
||||
cat > /www/wpad/wpad.dat << 'PACEOF'
|
||||
function FindProxyForURL(url, host) {
|
||||
if (isPlainHostName(host) || shExpMatch(host, "*.local") || shExpMatch(host, "*.lan") ||
|
||||
isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
|
||||
isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
|
||||
isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") ||
|
||||
isInNet(dnsResolve(host), "127.0.0.0", "255.0.0.0")) {
|
||||
return "DIRECT";
|
||||
}
|
||||
return "PROXY 192.168.255.1:8080; DIRECT";
|
||||
}
|
||||
PACEOF
|
||||
uci set dhcp.lan.dhcp_option="252,http://192.168.255.1/wpad/wpad.dat"
|
||||
uci commit dhcp
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "mitmproxy mode enabled - all traffic inspectable"
|
||||
;;
|
||||
*)
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Unknown mode: $mode"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Restart dnsmasq to apply DHCP changes
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1 &
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
json_init
|
||||
|
||||
Loading…
Reference in New Issue
Block a user