feat(streamlit): Gitea auto-push, WAF integration, and rename enhancements

- Add auto Gitea push on emancipate and app rename
- Route emancipated instances through mitmproxy_inspector (WAF) by default
- Add mitmproxy route entries for domains
- Enhanced rename_app to actually rename folders/files
- Enhanced rename_instance to update HAProxy vhost and mitmproxy routes
- Display WAF badge in dashboard for exposed instances

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-25 10:46:01 +01:00
parent 2335578203
commit dd9d1f1236
3 changed files with 219 additions and 21 deletions

View File

@ -3571,3 +3571,24 @@ git checkout HEAD -- index.html
- `secubox-app-haproxy/files/usr/sbin/haproxyctl`: Added lxc_start_bg, lxc_reload; fixed ACME cert handling - `secubox-app-haproxy/files/usr/sbin/haproxyctl`: Added lxc_start_bg, lxc_reload; fixed ACME cert handling
- `secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs`: Uses haproxyctl reload instead of init script - `secubox-app-haproxy/files/usr/sbin/haproxy-sync-certs`: Uses haproxyctl reload instead of init script
- **Verified:** 20 consecutive tests all returned HTTP 200 across all sites - **Verified:** 20 consecutive tests all returned HTTP 200 across all sites
32. **Streamlit Gitea Integration & WAF Enhancements (2026-02-25)**
- **Auto Gitea Push on Emancipate:**
- Added automatic Gitea push when instance is emancipated
- Also pushes on app rename (keeps code in sync with Gitea)
- **WAF (mitmproxy) Integration:**
- Emancipate now routes through `mitmproxy_inspector` backend by default (all traffic WAF-protected)
- Adds mitmproxy route entry for domain → streamlit port
- Restarts mitmproxy to pick up new routes
- Uses `haproxyctl reload` instead of restart for smooth reloads
- **Enhanced Rename Functions:**
- `rename_app()` now actually renames app folder/file (not just display name)
- Updates all instance references when app ID changes
- `rename_instance()` can now change domain, updates HAProxy vhost and mitmproxy routes
- **WAF Status Display:**
- Dashboard shows WAF badge for exposed instances
- `get_exposure_status()` returns `waf_enabled` field
- Blue "WAF" badge displayed next to exposure status
- **Files Modified:**
- `luci-app-streamlit/root/usr/libexec/rpcd/luci.streamlit`: emancipate_instance, rename_app, rename_instance, get_exposure_status
- `luci-app-streamlit/htdocs/luci-static/resources/view/streamlit/dashboard.js`: WAF badge display

View File

@ -146,6 +146,7 @@ return view.extend({
var isExposed = exp.emancipated; var isExposed = exp.emancipated;
var certValid = exp.cert_valid; var certValid = exp.cert_valid;
var authRequired = exp.auth_required; var authRequired = exp.auth_required;
var wafEnabled = exp.waf_enabled;
// Status indicator // Status indicator
var statusBadge; var statusBadge;
@ -159,6 +160,15 @@ return view.extend({
statusBadge = E('span', { 'style': 'color:#999' }, _('Local only')); statusBadge = E('span', { 'style': 'color:#999' }, _('Local only'));
} }
// WAF badge (shown when exposed)
var wafBadge = '';
if (isExposed && wafEnabled) {
wafBadge = E('span', {
'style': 'display:inline-block; padding:2px 6px; border-radius:4px; font-size:0.85em; background:#d1ecf1; color:#0c5460; margin-left:4px',
'title': _('Traffic inspected by WAF (mitmproxy)')
}, 'WAF');
}
// Running indicator // Running indicator
var runStatus = inst.enabled ? var runStatus = inst.enabled ?
E('span', { 'style': 'color:#0a0' }, '\u25CF') : E('span', { 'style': 'color:#0a0' }, '\u25CF') :
@ -221,7 +231,7 @@ return view.extend({
E('td', {}, [runStatus, ' ', E('strong', {}, inst.id)]), E('td', {}, [runStatus, ' ', E('strong', {}, inst.id)]),
E('td', {}, inst.app || '-'), E('td', {}, inst.app || '-'),
E('td', {}, ':' + inst.port), E('td', {}, ':' + inst.port),
E('td', {}, statusBadge), E('td', {}, [statusBadge, wafBadge]),
E('td', {}, actions) E('td', {}, actions)
]); ]);
}); });

View File

@ -796,37 +796,98 @@ remove_instance() {
json_success "Instance removed: $id" json_success "Instance removed: $id"
} }
# Rename app # Rename app (updates both display name and file/folder name)
rename_app() { rename_app() {
read -r input read -r input
local id name local id new_name new_id
id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) new_name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
new_id=$(echo "$input" | jsonfilter -e '@.new_id' 2>/dev/null)
if [ -z "$id" ] || [ -z "$name" ]; then if [ -z "$id" ] || [ -z "$new_name" ]; then
json_error "Missing id or name" json_error "Missing id or name"
return return
fi fi
# Create UCI section if it doesn't exist yet # If new_id not provided, sanitize new_name to create it
[ -z "$new_id" ] && new_id=$(echo "$new_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')
local data_path=$(uci -q get "${CONFIG}.main.data_path")
[ -z "$data_path" ] && data_path="/srv/streamlit"
local apps_path="$data_path/apps"
# Check if renaming filesystem (folder or file)
local old_path=""
local new_path=""
if [ -d "$apps_path/$id" ]; then
old_path="$apps_path/$id"
new_path="$apps_path/$new_id"
elif [ -f "$apps_path/${id}.py" ]; then
old_path="$apps_path/${id}.py"
new_path="$apps_path/${new_id}.py"
fi
# Rename filesystem if different
if [ -n "$old_path" ] && [ "$id" != "$new_id" ]; then
if [ -e "$new_path" ]; then
json_error "Destination already exists: $new_id"
return
fi
mv "$old_path" "$new_path"
fi
# Update display name in UCI
local existing local existing
existing=$(uci -q get "${CONFIG}.${id}") existing=$(uci -q get "${CONFIG}.${id}")
if [ -z "$existing" ]; then if [ -z "$existing" ]; then
uci set "${CONFIG}.${id}=app" uci set "${CONFIG}.${id}=app"
uci set "${CONFIG}.${id}.enabled=1" uci set "${CONFIG}.${id}.enabled=1"
fi fi
uci set "${CONFIG}.${id}.name=$new_name"
# If id changed, update all instance references
if [ "$id" != "$new_id" ]; then
config_load "$CONFIG"
_update_instance_refs() {
local section="$1"
local app
app=$(uci -q get "${CONFIG}.${section}.app")
if [ "$app" = "$id" ] || [ "$app" = "${id}.py" ]; then
# Update to new app reference
if [ -d "$new_path" ]; then
uci set "${CONFIG}.${section}.app=$new_id"
else
uci set "${CONFIG}.${section}.app=${new_id}.py"
fi
fi
}
config_foreach _update_instance_refs instance
fi
uci set "${CONFIG}.${id}.name=$name"
uci commit "$CONFIG" uci commit "$CONFIG"
json_success "App renamed"
# Auto-push to Gitea if configured
local gitea_enabled=$(uci -q get "${CONFIG}.gitea.enabled")
if [ "$gitea_enabled" = "1" ]; then
streamlitctl gitea push "$new_id" >/dev/null 2>&1 &
fi
json_init_obj
json_add_boolean "success" 1
json_add_string "message" "App renamed"
json_add_string "old_id" "$id"
json_add_string "new_id" "$new_id"
json_add_string "name" "$new_name"
json_close_obj
} }
# Rename instance # Rename instance (updates display name and optionally domain/vhost)
rename_instance() { rename_instance() {
read -r input read -r input
local id name local id name new_domain
id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
new_domain=$(echo "$input" | jsonfilter -e '@.domain' 2>/dev/null)
if [ -z "$id" ] || [ -z "$name" ]; then if [ -z "$id" ] || [ -z "$name" ]; then
json_error "Missing id or name" json_error "Missing id or name"
@ -840,9 +901,75 @@ rename_instance() {
return return
fi fi
# Update display name
uci set "${CONFIG}.${id}.name=$name" uci set "${CONFIG}.${id}.name=$name"
# If new domain provided and instance is emancipated, update vhost
local emancipated=$(uci -q get "${CONFIG}.${id}.emancipated")
local old_domain=$(uci -q get "${CONFIG}.${id}.domain")
local port=$(uci -q get "${CONFIG}.${id}.port")
if [ "$emancipated" = "1" ] && [ -n "$new_domain" ] && [ "$new_domain" != "$old_domain" ]; then
local old_vhost=$(echo "$old_domain" | sed 's/\./_/g')
local new_vhost=$(echo "$new_domain" | sed 's/\./_/g')
local backend_name="streamlit_${id}"
# Remove old vhost and cert entries
uci delete "haproxy.${old_vhost}" 2>/dev/null
uci delete "haproxy.cert_${old_vhost}" 2>/dev/null
# Create new vhost with WAF routing
uci set "haproxy.${new_vhost}=vhost"
uci set "haproxy.${new_vhost}.domain=${new_domain}"
uci set "haproxy.${new_vhost}.backend=mitmproxy_inspector"
uci set "haproxy.${new_vhost}.ssl=1"
uci set "haproxy.${new_vhost}.ssl_redirect=1"
uci set "haproxy.${new_vhost}.acme=1"
uci set "haproxy.${new_vhost}.enabled=1"
# Create new certificate entry
uci set "haproxy.cert_${new_vhost}=certificate"
uci set "haproxy.cert_${new_vhost}.domain=${new_domain}"
uci set "haproxy.cert_${new_vhost}.type=acme"
uci set "haproxy.cert_${new_vhost}.enabled=1"
uci commit haproxy
# Update mitmproxy routes
local routes_file="/srv/mitmproxy/haproxy-routes.json"
if [ -f "$routes_file" ]; then
# Remove old route and add new one
if command -v jq >/dev/null 2>&1; then
jq --arg old "$old_domain" --arg new "$new_domain" --argjson port "$port" \
'del(.[$old]) | . + {($new): ["192.168.255.1", $port]}' "$routes_file" > "/tmp/routes_$$.json" && \
mv "/tmp/routes_$$.json" "$routes_file"
fi
fi
# Reload HAProxy and mitmproxy
haproxyctl generate >/dev/null 2>&1
haproxyctl reload >/dev/null 2>&1
/etc/init.d/mitmproxy restart >/dev/null 2>&1 &
# Update instance domain
uci set "${CONFIG}.${id}.domain=${new_domain}"
# Request new certificate
case "$new_domain" in
*.gk2.secubox.in) ;;
*) haproxyctl cert add "$new_domain" >/dev/null 2>&1 & ;;
esac
fi
uci commit "$CONFIG" uci commit "$CONFIG"
json_success "Instance renamed"
json_init_obj
json_add_boolean "success" 1
json_add_string "message" "Instance renamed"
json_add_string "id" "$id"
json_add_string "name" "$name"
[ -n "$new_domain" ] && json_add_string "domain" "$new_domain"
json_close_obj
} }
# Enable instance # Enable instance
@ -1618,7 +1745,7 @@ emancipate_instance() {
local vhost_section=$(echo "$domain" | sed 's/\./_/g') local vhost_section=$(echo "$domain" | sed 's/\./_/g')
local backend_name="streamlit_${id}" local backend_name="streamlit_${id}"
# Create backend # Create backend for direct routing (used by mitmproxy)
uci set "haproxy.${backend_name}=backend" uci set "haproxy.${backend_name}=backend"
uci set "haproxy.${backend_name}.name=${backend_name}" uci set "haproxy.${backend_name}.name=${backend_name}"
uci set "haproxy.${backend_name}.mode=http" uci set "haproxy.${backend_name}.mode=http"
@ -1635,10 +1762,10 @@ emancipate_instance() {
uci set "haproxy.${backend_name}_srv.check=1" uci set "haproxy.${backend_name}_srv.check=1"
uci set "haproxy.${backend_name}_srv.enabled=1" uci set "haproxy.${backend_name}_srv.enabled=1"
# Create vhost - NO waf_bypass (all traffic through mitmproxy) # Create vhost - Route through mitmproxy_inspector for WAF protection
uci set "haproxy.${vhost_section}=vhost" uci set "haproxy.${vhost_section}=vhost"
uci set "haproxy.${vhost_section}.domain=${domain}" uci set "haproxy.${vhost_section}.domain=${domain}"
uci set "haproxy.${vhost_section}.backend=${backend_name}" uci set "haproxy.${vhost_section}.backend=mitmproxy_inspector"
uci set "haproxy.${vhost_section}.ssl=1" uci set "haproxy.${vhost_section}.ssl=1"
uci set "haproxy.${vhost_section}.ssl_redirect=1" uci set "haproxy.${vhost_section}.ssl_redirect=1"
uci set "haproxy.${vhost_section}.acme=1" uci set "haproxy.${vhost_section}.acme=1"
@ -1652,14 +1779,38 @@ emancipate_instance() {
uci commit haproxy uci commit haproxy
# Sync mitmproxy routes from HAProxy config # Add mitmproxy route for this domain -> streamlit backend
if command -v mitmproxyctl >/dev/null 2>&1; then local routes_file="/srv/mitmproxy/haproxy-routes.json"
mitmproxyctl sync-routes >/dev/null 2>&1 local routes_file_in="/srv/mitmproxy-in/haproxy-routes.json"
if [ -f "$routes_file" ]; then
# Add route entry: "domain": ["192.168.255.1", port]
local tmp_routes="/tmp/routes_$$.json"
if command -v jq >/dev/null 2>&1; then
jq --arg domain "$domain" --argjson port "$port" \
'. + {($domain): ["192.168.255.1", $port]}' "$routes_file" > "$tmp_routes" 2>/dev/null && \
mv "$tmp_routes" "$routes_file"
else
# Fallback: append using sed (for OpenWrt without jq)
sed -i "s/}$/,\"${domain}\":[\"192.168.255.1\",${port}]}/" "$routes_file" 2>/dev/null
fi
fi
# Same for inbound mitmproxy
if [ -f "$routes_file_in" ]; then
if command -v jq >/dev/null 2>&1; then
jq --arg domain "$domain" --argjson port "$port" \
'. + {($domain): ["192.168.255.1", $port]}' "$routes_file_in" > "/tmp/routes_in_$$.json" 2>/dev/null && \
mv "/tmp/routes_in_$$.json" "$routes_file_in"
else
sed -i "s/}$/,\"${domain}\":[\"192.168.255.1\",${port}]}/" "$routes_file_in" 2>/dev/null
fi
fi fi
# Regenerate and restart HAProxy for clean state # Restart mitmproxy to pick up routes
/etc/init.d/mitmproxy restart >/dev/null 2>&1 &
# Regenerate and reload HAProxy
haproxyctl generate >/dev/null 2>&1 haproxyctl generate >/dev/null 2>&1
/etc/init.d/haproxy restart >/dev/null 2>&1 haproxyctl reload >/dev/null 2>&1
# Request certificate via ACME (wildcard covers *.gk2.secubox.in) # Request certificate via ACME (wildcard covers *.gk2.secubox.in)
case "$domain" in case "$domain" in
@ -1675,14 +1826,22 @@ emancipate_instance() {
uci set "${CONFIG}.${id}.emancipated=1" uci set "${CONFIG}.${id}.emancipated=1"
uci set "${CONFIG}.${id}.emancipated_at=$(date -Iseconds)" uci set "${CONFIG}.${id}.emancipated_at=$(date -Iseconds)"
uci set "${CONFIG}.${id}.domain=${domain}" uci set "${CONFIG}.${id}.domain=${domain}"
uci set "${CONFIG}.${id}.waf_enabled=1"
uci commit "$CONFIG" uci commit "$CONFIG"
# Auto-push to Gitea if configured
local gitea_enabled=$(uci -q get "${CONFIG}.gitea.enabled")
if [ "$gitea_enabled" = "1" ]; then
streamlitctl gitea push "$app" >/dev/null 2>&1 &
fi
json_init_obj json_init_obj
json_add_boolean "success" 1 json_add_boolean "success" 1
json_add_string "message" "Instance exposed at https://${domain}" json_add_string "message" "Instance exposed at https://${domain} (WAF protected)"
json_add_string "domain" "$domain" json_add_string "domain" "$domain"
json_add_string "url" "https://${domain}" json_add_string "url" "https://${domain}"
json_add_int "port" "$port" json_add_int "port" "$port"
json_add_boolean "waf_enabled" 1
json_close_obj json_close_obj
} }
@ -1695,7 +1854,7 @@ get_exposure_status() {
_add_exposure_json() { _add_exposure_json() {
local section="$1" local section="$1"
local app port enabled domain emancipated auth_required local app port enabled domain emancipated auth_required waf_enabled
config_get app "$section" app "" config_get app "$section" app ""
config_get port "$section" port "" config_get port "$section" port ""
@ -1703,6 +1862,7 @@ get_exposure_status() {
config_get domain "$section" domain "" config_get domain "$section" domain ""
config_get emancipated "$section" emancipated "0" config_get emancipated "$section" emancipated "0"
config_get auth_required "$section" auth_required "0" config_get auth_required "$section" auth_required "0"
config_get waf_enabled "$section" waf_enabled "0"
[ -z "$app" ] && return [ -z "$app" ] && return
@ -1714,6 +1874,12 @@ get_exposure_status() {
cert_valid=1 cert_valid=1
cert_expires=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) cert_expires=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
fi fi
# Check WAF status from HAProxy vhost config
local vhost_section=$(echo "$domain" | sed 's/\./_/g')
local vhost_backend=$(uci -q get "haproxy.${vhost_section}.backend" 2>/dev/null)
if [ "$vhost_backend" = "mitmproxy_inspector" ]; then
waf_enabled=1
fi
fi fi
json_add_object "" json_add_object ""
@ -1726,6 +1892,7 @@ get_exposure_status() {
json_add_boolean "auth_required" "$( [ "$auth_required" = "1" ] && echo 1 || echo 0 )" json_add_boolean "auth_required" "$( [ "$auth_required" = "1" ] && echo 1 || echo 0 )"
json_add_boolean "cert_valid" "$cert_valid" json_add_boolean "cert_valid" "$cert_valid"
json_add_string "cert_expires" "$cert_expires" json_add_string "cert_expires" "$cert_expires"
json_add_boolean "waf_enabled" "$( [ "$waf_enabled" = "1" ] && echo 1 || echo 0 )"
json_close_object json_close_object
} }