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:
CyberMind-FR 2026-01-28 13:13:10 +01:00
parent 14af23774a
commit 906bf6f549
23 changed files with 1021 additions and 90 deletions

View File

@ -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

View File

@ -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>

View File

@ -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({

View File

@ -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
});

View File

@ -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
]);
});
}
});

View File

@ -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": {

View File

@ -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

View File

@ -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
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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",

View File

@ -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

View File

@ -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
============================================ */

View File

@ -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));
}
}
});
}
},

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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