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/haproxy-sync-certs`: Uses haproxyctl reload instead of init script
- **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 certValid = exp.cert_valid;
var authRequired = exp.auth_required;
var wafEnabled = exp.waf_enabled;
// Status indicator
var statusBadge;
@ -159,6 +160,15 @@ return view.extend({
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
var runStatus = inst.enabled ?
E('span', { 'style': 'color:#0a0' }, '\u25CF') :
@ -221,7 +231,7 @@ return view.extend({
E('td', {}, [runStatus, ' ', E('strong', {}, inst.id)]),
E('td', {}, inst.app || '-'),
E('td', {}, ':' + inst.port),
E('td', {}, statusBadge),
E('td', {}, [statusBadge, wafBadge]),
E('td', {}, actions)
]);
});

View File

@ -796,37 +796,98 @@ remove_instance() {
json_success "Instance removed: $id"
}
# Rename app
# Rename app (updates both display name and file/folder name)
rename_app() {
read -r input
local id name
local id new_name new_id
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"
return
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
existing=$(uci -q get "${CONFIG}.${id}")
if [ -z "$existing" ]; then
uci set "${CONFIG}.${id}=app"
uci set "${CONFIG}.${id}.enabled=1"
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"
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() {
read -r input
local id name
local id name new_domain
id=$(echo "$input" | jsonfilter -e '@.id' 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
json_error "Missing id or name"
@ -840,9 +901,75 @@ rename_instance() {
return
fi
# Update display 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"
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
@ -1618,7 +1745,7 @@ emancipate_instance() {
local vhost_section=$(echo "$domain" | sed 's/\./_/g')
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}.name=${backend_name}"
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.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}.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_redirect=1"
uci set "haproxy.${vhost_section}.acme=1"
@ -1652,14 +1779,38 @@ emancipate_instance() {
uci commit haproxy
# Sync mitmproxy routes from HAProxy config
if command -v mitmproxyctl >/dev/null 2>&1; then
mitmproxyctl sync-routes >/dev/null 2>&1
# Add mitmproxy route for this domain -> streamlit backend
local routes_file="/srv/mitmproxy/haproxy-routes.json"
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
# 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
/etc/init.d/haproxy restart >/dev/null 2>&1
haproxyctl reload >/dev/null 2>&1
# Request certificate via ACME (wildcard covers *.gk2.secubox.in)
case "$domain" in
@ -1675,14 +1826,22 @@ emancipate_instance() {
uci set "${CONFIG}.${id}.emancipated=1"
uci set "${CONFIG}.${id}.emancipated_at=$(date -Iseconds)"
uci set "${CONFIG}.${id}.domain=${domain}"
uci set "${CONFIG}.${id}.waf_enabled=1"
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_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 "url" "https://${domain}"
json_add_int "port" "$port"
json_add_boolean "waf_enabled" 1
json_close_obj
}
@ -1695,7 +1854,7 @@ get_exposure_status() {
_add_exposure_json() {
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 port "$section" port ""
@ -1703,6 +1862,7 @@ get_exposure_status() {
config_get domain "$section" domain ""
config_get emancipated "$section" emancipated "0"
config_get auth_required "$section" auth_required "0"
config_get waf_enabled "$section" waf_enabled "0"
[ -z "$app" ] && return
@ -1714,6 +1874,12 @@ get_exposure_status() {
cert_valid=1
cert_expires=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
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
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 "cert_valid" "$cert_valid"
json_add_string "cert_expires" "$cert_expires"
json_add_boolean "waf_enabled" "$( [ "$waf_enabled" = "1" ] && echo 1 || echo 0 )"
json_close_object
}