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
|
## 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
|
### Local Feeds Hygiene
|
||||||
- Clean and resync local feeds before build iterations when dependency drift is suspected
|
- Clean and resync local feeds before build iterations when dependency drift is suspected
|
||||||
- Prefer the repo helpers; avoid ad-hoc `rm` unless explicitly needed
|
- 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_NAME:=luci-app-cdn-cache
|
||||||
PKG_VERSION:=0.5.0
|
PKG_VERSION:=0.5.0
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
'require baseclass';
|
'require baseclass';
|
||||||
|
|
||||||
var tabs = [
|
var tabs = [
|
||||||
{ id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'overview'] },
|
{ id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'services', 'cdn-cache', 'overview'] },
|
||||||
{ id: 'cache', icon: '💾', label: _('Cache'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'cache'] },
|
{ id: 'cache', icon: '💾', label: _('Cache'), path: ['admin', 'services', 'cdn-cache', 'cache'] },
|
||||||
{ id: 'policies', icon: '🧭', label: _('Policies'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'policies'] },
|
{ id: 'policies', icon: '🧭', label: _('Policies'), path: ['admin', 'services', 'cdn-cache', 'policies'] },
|
||||||
{ id: 'statistics', icon: '📊', label: _('Statistics'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'statistics'] },
|
{ id: 'statistics', icon: '📊', label: _('Statistics'), path: ['admin', 'services', 'cdn-cache', 'statistics'] },
|
||||||
{ id: 'maintenance', icon: '🧹', label: _('Maintenance'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'maintenance'] },
|
{ id: 'maintenance', icon: '🧹', label: _('Maintenance'), path: ['admin', 'services', 'cdn-cache', 'maintenance'] },
|
||||||
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'settings'] }
|
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'services', 'cdn-cache', 'settings'] }
|
||||||
];
|
];
|
||||||
|
|
||||||
return baseclass.extend({
|
return baseclass.extend({
|
||||||
|
|||||||
@ -49,22 +49,23 @@ function formatUptime(seconds) {
|
|||||||
return minutes + 'm ' + (seconds % 60) + 's';
|
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({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
callStatus(),
|
L.resolveDefault(callStatus(), {}),
|
||||||
callStats(),
|
L.resolveDefault(callStats(), {}),
|
||||||
callCacheSize(),
|
L.resolveDefault(callCacheSize(), {}),
|
||||||
callTopDomains()
|
L.resolveDefault(callTopDomains(), { domains: [] })
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
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 status = data[0] || {};
|
||||||
var stats = data[1] || {};
|
var stats = data[1] || {};
|
||||||
var cacheSize = data[2] || {};
|
var cacheSize = data[2] || {};
|
||||||
@ -204,5 +205,9 @@ return view.extend({
|
|||||||
]),
|
]),
|
||||||
E('div', { 'class': 'secubox-stat-label' }, meta)
|
E('div', { 'class': 'secubox-stat-label' }, meta)
|
||||||
]);
|
]);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
});
|
});
|
||||||
|
|||||||
@ -138,11 +138,13 @@ return view.extend({
|
|||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
o.default = '60';
|
o.default = '60';
|
||||||
|
|
||||||
return E('div', { 'class': 'cdn-settings-page' }, [
|
return m.render().then(function(formEl) {
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
return E('div', { 'class': 'cdn-settings-page' }, [
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||||
CdnNav.renderTabs('settings'),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
|
||||||
m.render()
|
CdnNav.renderTabs('settings'),
|
||||||
]);
|
formEl
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"admin/secubox/network/cdn-cache": {
|
"admin/services/cdn-cache": {
|
||||||
"title": "CDN Cache",
|
"title": "CDN Cache",
|
||||||
"order": 30,
|
"order": 35,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "firstchild"
|
"type": "firstchild"
|
||||||
},
|
},
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"uci": {"cdn-cache": true}
|
"uci": {"cdn-cache": true}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/overview": {
|
"admin/services/cdn-cache/overview": {
|
||||||
"title": "Overview",
|
"title": "Overview",
|
||||||
"order": 10,
|
"order": 10,
|
||||||
"action": {
|
"action": {
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"path": "cdn-cache/overview"
|
"path": "cdn-cache/overview"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/cache": {
|
"admin/services/cdn-cache/cache": {
|
||||||
"title": "Cache",
|
"title": "Cache",
|
||||||
"order": 20,
|
"order": 20,
|
||||||
"action": {
|
"action": {
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"path": "cdn-cache/cache"
|
"path": "cdn-cache/cache"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/policies": {
|
"admin/services/cdn-cache/policies": {
|
||||||
"title": "Policies",
|
"title": "Policies",
|
||||||
"order": 30,
|
"order": 30,
|
||||||
"action": {
|
"action": {
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"path": "cdn-cache/policies"
|
"path": "cdn-cache/policies"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/statistics": {
|
"admin/services/cdn-cache/statistics": {
|
||||||
"title": "Statistics",
|
"title": "Statistics",
|
||||||
"order": 40,
|
"order": 40,
|
||||||
"action": {
|
"action": {
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"path": "cdn-cache/statistics"
|
"path": "cdn-cache/statistics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/maintenance": {
|
"admin/services/cdn-cache/maintenance": {
|
||||||
"title": "Maintenance",
|
"title": "Maintenance",
|
||||||
"order": 50,
|
"order": 50,
|
||||||
"action": {
|
"action": {
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"path": "cdn-cache/maintenance"
|
"path": "cdn-cache/maintenance"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/network/cdn-cache/settings": {
|
"admin/services/cdn-cache/settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"order": 90,
|
"order": 90,
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-metablogizer
|
PKG_NAME:=luci-app-metablogizer
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
PKG_LICENSE:=GPL-2.0
|
PKG_LICENSE:=GPL-2.0
|
||||||
|
|
||||||
|
|||||||
@ -776,31 +776,104 @@ get_public_ipv4() {
|
|||||||
echo "$ip"
|
echo "$ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: Check certificate expiry
|
# Helper: Check certificate expiry (BusyBox compatible)
|
||||||
check_cert_expiry() {
|
check_cert_expiry() {
|
||||||
local domain="$1"
|
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"
|
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
|
fi
|
||||||
|
|
||||||
if [ -f "$cert_file" ]; then
|
if [ -z "$cert_file" ] || [ ! -f "$cert_file" ]; then
|
||||||
local expiry_date
|
return 1
|
||||||
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
|
|
||||||
fi
|
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
|
# Get hosting status for all sites with DNS and cert health
|
||||||
@ -909,6 +982,46 @@ _add_site_health() {
|
|||||||
json_add_string "cert_status" "none"
|
json_add_string "cert_status" "none"
|
||||||
fi
|
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
|
# Publish status
|
||||||
local publish_status="draft"
|
local publish_status="draft"
|
||||||
if [ "$enabled" = "1" ] && [ "$has_content" = "1" ]; then
|
if [ "$enabled" = "1" ] && [ "$has_content" = "1" ]; then
|
||||||
@ -942,10 +1055,11 @@ method_check_site_health() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local name domain ssl
|
local name domain ssl port
|
||||||
name=$(get_uci "$id" name "")
|
name=$(get_uci "$id" name "")
|
||||||
domain=$(get_uci "$id" domain "")
|
domain=$(get_uci "$id" domain "")
|
||||||
ssl=$(get_uci "$id" ssl "1")
|
ssl=$(get_uci "$id" ssl "1")
|
||||||
|
port=$(get_uci "$id" port "")
|
||||||
|
|
||||||
if [ -z "$name" ]; then
|
if [ -z "$name" ]; then
|
||||||
json_init
|
json_init
|
||||||
@ -1048,6 +1162,65 @@ method_check_site_health() {
|
|||||||
fi
|
fi
|
||||||
json_close_object
|
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
|
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_DEPENDS:=+luci-base +luci-theme-secubox
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
PKG_VERSION:=0.7.0
|
PKG_VERSION:=0.7.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
PKG_MAINTAINER:=SecuBox Team <secubox@example.com>
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,17 @@ var callDashboardData = rpc.declare({
|
|||||||
expect: { counts: {} }
|
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({
|
return view.extend({
|
||||||
currentSection: 'dashboard',
|
currentSection: 'dashboard',
|
||||||
appStatuses: {},
|
appStatuses: {},
|
||||||
@ -55,7 +66,8 @@ return view.extend({
|
|||||||
portal.checkInstalledApps(),
|
portal.checkInstalledApps(),
|
||||||
callGetServices().catch(function() { return []; }),
|
callGetServices().catch(function() { return []; }),
|
||||||
callSecurityStats().catch(function() { return null; }),
|
callSecurityStats().catch(function() { return null; }),
|
||||||
callDashboardData().catch(function() { return { counts: {} }; })
|
callDashboardData().catch(function() { return { counts: {} }; }),
|
||||||
|
callGetProxyMode().catch(function() { return { mode: 'direct' }; })
|
||||||
]).then(function(results) {
|
]).then(function(results) {
|
||||||
// Store installed apps info from the last promise
|
// Store installed apps info from the last promise
|
||||||
self.installedApps = results[4] || {};
|
self.installedApps = results[4] || {};
|
||||||
@ -66,6 +78,8 @@ return view.extend({
|
|||||||
self.detectedServices = Array.isArray(svcResult) ? svcResult : (svcResult.services || []);
|
self.detectedServices = Array.isArray(svcResult) ? svcResult : (svcResult.services || []);
|
||||||
// Security stats
|
// Security stats
|
||||||
self.securityStats = results[6] || {};
|
self.securityStats = results[6] || {};
|
||||||
|
// Proxy mode
|
||||||
|
self.proxyMode = results[8] || { mode: 'direct' };
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -324,6 +338,12 @@ return view.extend({
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Proxy Mode Switcher
|
||||||
|
this.renderProxyModeSwitcher(),
|
||||||
|
|
||||||
|
// Speed Test Widget
|
||||||
|
this.renderSpeedTestWidget(),
|
||||||
|
|
||||||
// Featured Apps
|
// Featured Apps
|
||||||
E('h3', { 'style': 'margin: 1.5rem 0 1rem; color: var(--cyber-text-primary);' }, 'Quick Access'),
|
E('h3', { 'style': 'margin: 1.5rem 0 1rem; color: var(--cyber-text-primary);' }, 'Quick Access'),
|
||||||
E('div', { 'class': 'sb-app-grid' },
|
E('div', { 'class': 'sb-app-grid' },
|
||||||
@ -631,6 +651,345 @@ return view.extend({
|
|||||||
return bytes.toFixed(1) + ' ' + units[i];
|
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,
|
handleSaveApply: null,
|
||||||
handleSave: null,
|
handleSave: null,
|
||||||
handleReset: null
|
handleReset: null
|
||||||
|
|||||||
@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-secubox-security-threats
|
PKG_NAME:=luci-app-secubox-security-threats
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|||||||
@ -275,16 +275,28 @@ get_security_stats() {
|
|||||||
local haproxy_conns=0
|
local haproxy_conns=0
|
||||||
local invalid_conns=0
|
local invalid_conns=0
|
||||||
|
|
||||||
# WAN dropped packets (from kernel stats)
|
# Get actual WAN interface from UCI
|
||||||
if [ -f /sys/class/net/br-wan/statistics/rx_dropped ]; then
|
local wan_iface=$(uci -q get network.wan.device || uci -q get network.wan.ifname)
|
||||||
wan_drops=$(cat /sys/class/net/br-wan/statistics/rx_dropped 2>/dev/null)
|
[ -z "$wan_iface" ] && wan_iface="eth0"
|
||||||
elif [ -f /sys/class/net/eth1/statistics/rx_dropped ]; then
|
|
||||||
wan_drops=$(cat /sys/class/net/eth1/statistics/rx_dropped 2>/dev/null)
|
# 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
|
fi
|
||||||
wan_drops=${wan_drops:-0}
|
wan_drops=${wan_drops:-0}
|
||||||
|
|
||||||
# Firewall rejects from logs (last 24h)
|
# Firewall rejects - count from nftables counters (more accurate than logs)
|
||||||
fw_rejects=$(logread 2>/dev/null | grep -c "reject\|drop" || echo 0)
|
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=$(echo "$fw_rejects" | tr -d '\n')
|
||||||
fw_rejects=${fw_rejects:-0}
|
fw_rejects=${fw_rejects:-0}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-secubox
|
PKG_NAME:=luci-app-secubox
|
||||||
PKG_VERSION:=0.7.1
|
PKG_VERSION:=0.7.1
|
||||||
PKG_RELEASE:=3
|
PKG_RELEASE:=4
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,9 @@
|
|||||||
"get_public_ips",
|
"get_public_ips",
|
||||||
"get_network_health",
|
"get_network_health",
|
||||||
"get_vital_services",
|
"get_vital_services",
|
||||||
"get_full_health_report"
|
"get_full_health_report",
|
||||||
|
"get_services",
|
||||||
|
"get_proxy_mode"
|
||||||
],
|
],
|
||||||
"uci": [
|
"uci": [
|
||||||
"get",
|
"get",
|
||||||
@ -71,7 +73,8 @@
|
|||||||
"apply_profile",
|
"apply_profile",
|
||||||
"rollback_profile",
|
"rollback_profile",
|
||||||
"install_appstore_app",
|
"install_appstore_app",
|
||||||
"remove_appstore_app"
|
"remove_appstore_app",
|
||||||
|
"set_proxy_mode"
|
||||||
],
|
],
|
||||||
"uci": [
|
"uci": [
|
||||||
"set",
|
"set",
|
||||||
|
|||||||
@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-streamlit
|
PKG_NAME:=luci-app-streamlit
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=8
|
PKG_RELEASE:=9
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|||||||
@ -424,6 +424,97 @@
|
|||||||
color: #0ff;
|
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
|
Empty State
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@ -122,7 +122,7 @@ return view.extend({
|
|||||||
return E('div', { 'class': 'st-main-grid' }, [
|
return E('div', { 'class': 'st-main-grid' }, [
|
||||||
this.renderControlCard(),
|
this.renderControlCard(),
|
||||||
this.renderInfoCard(),
|
this.renderInfoCard(),
|
||||||
this.renderLogsCard()
|
this.renderInstancesCard()
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -236,26 +236,69 @@ return view.extend({
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLogsCard: function() {
|
renderInstancesCard: function() {
|
||||||
var logs = this.logsData || [];
|
var apps = this.appsData || {};
|
||||||
|
var instances = apps.apps || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
return E('div', { 'class': 'st-card st-card-full' }, [
|
return E('div', { 'class': 'st-card st-card-full' }, [
|
||||||
E('div', { 'class': 'st-card-header' }, [
|
E('div', { 'class': 'st-card-header' }, [
|
||||||
E('div', { 'class': 'st-card-title' }, [
|
E('div', { 'class': 'st-card-title' }, [
|
||||||
E('span', {}, '\uD83D\uDCDC'),
|
E('span', {}, '\uD83D\uDCCA'),
|
||||||
' ' + _('Recent Logs')
|
' ' + _('Instances')
|
||||||
])
|
]),
|
||||||
|
E('a', {
|
||||||
|
'href': L.url('admin', 'services', 'streamlit', 'apps'),
|
||||||
|
'class': 'st-link'
|
||||||
|
}, _('Manage Apps') + ' \u2192')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'st-card-body' }, [
|
E('div', { 'class': 'st-card-body st-no-padding' }, [
|
||||||
logs.length > 0 ?
|
instances.length > 0 ?
|
||||||
E('div', { 'class': 'st-logs', 'id': 'st-logs' },
|
E('table', { 'class': 'st-instances-table', 'id': 'st-instances' }, [
|
||||||
logs.slice(-20).map(function(line) {
|
E('thead', {}, [
|
||||||
return E('div', { 'class': 'st-logs-line' }, line);
|
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' }, [
|
||||||
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCED'),
|
E('div', { 'class': 'st-empty-icon' }, '\uD83D\uDCE6'),
|
||||||
E('div', {}, _('No logs available'))
|
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';
|
statActive.textContent = status.active_app || 'hello';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update logs
|
// Update instances table status indicators
|
||||||
var logsContainer = document.getElementById('st-logs');
|
var instancesTable = document.getElementById('st-instances');
|
||||||
if (logsContainer && this.logsData) {
|
if (instancesTable) {
|
||||||
logsContainer.innerHTML = '';
|
var apps = this.appsData && this.appsData.apps || [];
|
||||||
this.logsData.slice(-20).forEach(function(line) {
|
var rows = instancesTable.querySelectorAll('tbody tr');
|
||||||
logsContainer.appendChild(E('div', { 'class': 'st-logs-line' }, line));
|
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_NAME:=luci-app-tor-shield
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
|
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
|
|||||||
@ -622,6 +622,13 @@ set_bridges() {
|
|||||||
# Now initialize output JSON
|
# Now initialize output JSON
|
||||||
json_init
|
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 "$enabled" ] && uci set tor-shield.bridges.enabled="$enabled"
|
||||||
[ -n "$type" ] && uci set tor-shield.bridges.type="$type"
|
[ -n "$type" ] && uci set tor-shield.bridges.type="$type"
|
||||||
|
|
||||||
@ -710,6 +717,28 @@ save_settings() {
|
|||||||
# Now initialize output JSON
|
# Now initialize output JSON
|
||||||
json_init
|
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
|
# Apply settings
|
||||||
[ -n "$mode" ] && uci set tor-shield.main.mode="$mode"
|
[ -n "$mode" ] && uci set tor-shield.main.mode="$mode"
|
||||||
[ -n "$dns_over_tor" ] && uci set tor-shield.main.dns_over_tor="$dns_over_tor"
|
[ -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_NAME:=secubox-app-haproxy
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=21
|
PKG_RELEASE:=23
|
||||||
|
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
@ -18,7 +18,7 @@ define Package/secubox-app-haproxy
|
|||||||
CATEGORY:=SecuBox
|
CATEGORY:=SecuBox
|
||||||
SUBMENU:=Services
|
SUBMENU:=Services
|
||||||
TITLE:=HAProxy Load Balancer & Reverse Proxy
|
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
|
PKGARCH:=all
|
||||||
endef
|
endef
|
||||||
|
|
||||||
@ -73,6 +73,22 @@ endef
|
|||||||
define Package/secubox-app-haproxy/postinst
|
define Package/secubox-app-haproxy/postinst
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
[ -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
|
# Sync existing ACME certificates on install
|
||||||
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@ -8,6 +8,30 @@ USE_PROCD=1
|
|||||||
|
|
||||||
NAME="haproxy"
|
NAME="haproxy"
|
||||||
PROG="/usr/sbin/haproxyctl"
|
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() {
|
start_service() {
|
||||||
local enabled
|
local enabled
|
||||||
@ -16,6 +40,9 @@ start_service() {
|
|||||||
|
|
||||||
[ "$enabled" = "1" ] || return 0
|
[ "$enabled" = "1" ] || return 0
|
||||||
|
|
||||||
|
# Ensure ACME challenge webserver is configured and running
|
||||||
|
setup_acme_webserver
|
||||||
|
|
||||||
# Sync ACME certificates to HAProxy format before starting
|
# Sync ACME certificates to HAProxy format before starting
|
||||||
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
/usr/sbin/haproxy-sync-certs 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=secubox-core
|
PKG_NAME:=secubox-core
|
||||||
PKG_VERSION:=0.10.0
|
PKG_VERSION:=0.10.0
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
PKG_LICENSE:=GPL-2.0
|
PKG_LICENSE:=GPL-2.0
|
||||||
PKG_MAINTAINER:=SecuBox Team
|
PKG_MAINTAINER:=SecuBox Team
|
||||||
|
|||||||
@ -237,6 +237,14 @@ case "$1" in
|
|||||||
json_add_object "get_services"
|
json_add_object "get_services"
|
||||||
json_close_object
|
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
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@ -1658,6 +1666,128 @@ case "$1" in
|
|||||||
json_dump
|
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
|
json_init
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user