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:
parent
2335578203
commit
dd9d1f1236
@ -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
|
||||
|
||||
@ -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)
|
||||
]);
|
||||
});
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user