#!/bin/sh CONFIG=haproxy_manager HAPROXY_CFG=/etc/haproxy.cfg TMP_CFG=/tmp/haproxy-manager-generated.cfg BACKUP_LIMIT=7 INCIDENT_LIMIT=7 FIREWALL_RULE_NAME='HAProxy Manager: WAN listeners' FIREWALL_DISABLED_PREFIX='HAProxy Manager disabled: ' uci_get() { local section="$1" local option="$2" local default="${3:-}" uci -q get "$CONFIG.$section.$option" 2>/dev/null || printf '%s\n' "$default" } backup_dir() { local dir dir="$(uci_get main backup_dir /root/haproxy-manager-backups)" is_safe_storage_dir "$dir" || { echo "Recovery point directory must be a plain path under /root or /mnt: $dir" >&2 return 1 } printf '%s\n' "$dir" } incident_dir() { local dir dir="$(uci_get main incident_dir /root/haproxy-manager-incidents)" is_safe_storage_dir "$dir" || { echo "Incident directory must be a plain path under /root or /mnt: $dir" >&2 return 1 } printf '%s\n' "$dir" } is_safe_storage_dir() { case "$1" in /root/*|/mnt/*) ;; *) return 1 ;; esac case "/${1#/}/" in */../*|*/./*|*[!A-Za-z0-9_./-]*) return 1 ;; esac return 0 } resolve_storage_dir() { local dir="$1" resolved [ -d "$dir" ] || return 1 resolved="$(cd "$dir" 2>/dev/null && pwd -P)" || return 1 is_safe_storage_dir "$resolved" || return 1 printf '%s\n' "$resolved" } is_safe_temp_path() { local path="$1" name name="${path#/tmp/}" [ "$path" = "/tmp/$name" ] || return 1 case "$name" in haproxy-manager-[A-Za-z0-9._-]*) return 0 ;; *) return 1 ;; esac } operation_lock_acquire() { local lock_dir="$1" lock_pid if mkdir "$lock_dir" 2>/dev/null; then printf '%s\n' "$$" > "$lock_dir/pid" return 0 fi lock_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)" case "$lock_pid" in ''|*[!0-9]*) lock_pid=0 ;; esac if [ "$lock_pid" -gt 1 ] 2>/dev/null && kill -0 "$lock_pid" 2>/dev/null; then return 1 fi rm -f "$lock_dir/pid" rmdir "$lock_dir" 2>/dev/null || return 1 mkdir "$lock_dir" 2>/dev/null || return 1 printf '%s\n' "$$" > "$lock_dir/pid" } operation_lock_set_pid() { printf '%s\n' "$2" > "$1/pid" } operation_lock_release() { rm -f "$1/pid" rmdir "$1" 2>/dev/null || true } atomic_install_file() { local source="$1" destination="$2" temporary mode [ -f "$source" ] || return 1 temporary="$destination.haproxy-manager.$$" rm -f "$temporary" if ! cp "$source" "$temporary"; then rm -f "$temporary" return 1 fi mode="$(stat -c '%a' "$destination" 2>/dev/null || stat -c '%a' "$source" 2>/dev/null || echo 600)" chmod "$mode" "$temporary" || { rm -f "$temporary"; return 1; } mv -f "$temporary" "$destination" || { rm -f "$temporary"; return 1; } } active_mode() { local mode mode="$(uci_get main active_mode)" case "$mode" in generated|raw|none) printf '%s\n' "$mode" ;; *) if [ "$(uci_get main enabled 0)" = 1 ] && grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then printf 'generated\n' else printf 'none\n' fi ;; esac } wan_interface() { local interface interface="$(uci_get main wan_interface wan)" case "$interface" in ''|*[!A-Za-z0-9_.-]*) return 1 ;; esac printf '%s\n' "$interface" } wan_ip() { local configured interface configured="$(uci_get main wan_bind_ip auto)" if [ "$configured" != auto ] && [ -n "$configured" ]; then printf '%s\n' "$configured" return 0 fi interface="$(wan_interface)" || return 1 ubus call "network.interface.$interface" status 2>/dev/null \ | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \ | grep -m1 . } lan_ip() { local configured configured="$(uci_get main lan_bind_ip auto)" if [ "$configured" != auto ] && [ -n "$configured" ]; then printf '%s\n' "$configured" return 0 fi ubus call network.interface.lan status 2>/dev/null \ | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \ | grep -m1 . } route_name() { printf '%s' "$1" | tr '.:-' '___' | tr -cd 'A-Za-z0-9_' } is_valid_host() { case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9]) return 1 ;; *[!A-Za-z0-9.-]*) return 1 ;; esac return 0 } is_valid_ip_or_host() { case "$1" in ''|*[!A-Za-z0-9_.:-]*) return 1 ;; esac return 0 } is_valid_backup_id() { case "$1" in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) return 0 ;; *) return 1 ;; esac } backend_address() { local host="$1" local port="$2" case "$host" in *:*) printf '[%s]:%s\n' "$host" "$port" ;; *) printf '%s:%s\n' "$host" "$port" ;; esac } is_valid_port() { local port="$1" [ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null } route_sections() { uci -q show "$CONFIG" \ | sed -n "s/^$CONFIG\\.\\(@route\\[[0-9][0-9]*\\]\\)=route/\\1/p" } route_kind() { local section="$1" local kind kind="$(uci_get "$section" kind)" [ -n "$kind" ] || kind="$(uci_get "$section" protocol http)" printf '%s\n' "$kind" } route_port_maps() { local section="$1" local kind="$2" local public_port backend_port case "$kind" in ssh) public_port="$(uci_get "$section" ssh_listen_port 22)" backend_port="$(uci_get "$section" ssh_backend_port 22)" printf '%s:%s\n' "$public_port" "$backend_port" ;; rdp) public_port="$(uci_get "$section" rdp_listen_port 3389)" backend_port="$(uci_get "$section" rdp_backend_port 3389)" printf '%s:%s\n' "$public_port" "$backend_port" ;; custom) for map in $(uci_get "$section" port_map); do printf '%s\n' "$map" done ;; tcp) public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")" backend_port="$(uci_get "$section" backend_port)" printf '%s:%s\n' "$public_port" "$backend_port" ;; esac } is_valid_port_map() { local map="$1" local public_port backend_port case "$map" in *:*:*) return 1 ;; *:*) ;; *) return 1 ;; esac public_port="${map%%:*}" backend_port="${map#*:}" is_valid_port "$public_port" && is_valid_port "$backend_port" } required_ports() { local section enabled kind http_enabled https_enabled map local http_port https_port http_port="$(uci_get main http_port 80)" https_port="$(uci_get main https_port 443)" [ "$(uci_get main enabled 0)" = 1 ] || return 0 for section in $(route_sections); do enabled="$(uci_get "$section" enabled 1)" [ "$enabled" = 1 ] || continue kind="$(route_kind "$section")" case "$kind" in web) http_enabled="$(uci_get "$section" web_http 1)" https_enabled="$(uci_get "$section" web_https 1)" [ "$http_enabled" != 1 ] || printf '%s\n' "$http_port" [ "$https_enabled" != 1 ] || printf '%s\n' "$https_port" ;; http) printf '%s\n' "$http_port" ;; https) printf '%s\n' "$https_port" ;; both) printf '%s\n%s\n' "$http_port" "$https_port" ;; ssh|rdp|custom|tcp) for map in $(route_port_maps "$section" "$kind"); do printf '%s\n' "${map%%:*}" done ;; esac done | sort -nu } port_spec_contains() { local spec="$1" local wanted="$2" local token first last for token in $(printf '%s' "$spec" | tr ',' ' '); do case "$token" in *-*) first="${token%%-*}" last="${token#*-}" [ "$wanted" -ge "$first" ] 2>/dev/null && [ "$wanted" -le "$last" ] 2>/dev/null && return 0 ;; *) [ "$token" = "$wanted" ] && return 0 ;; esac done return 1 } set_uhttpd_list() { local option values current address option="$1" values="$2" current="$(uci -q get "uhttpd.main.$option" 2>/dev/null || true)" [ "$current" = "$values" ] && return 0 uci -q delete "uhttpd.main.$option" || true for address in $values; do uci add_list "uhttpd.main.$option=$address" done UHTTPD_CHANGED=1 } capture_uhttpd_bindings() { local saved_http saved_https address [ "$(uci_get main uhttpd_bind_owned 0)" != 1 ] || return 0 saved_http="$(uci -q get uhttpd.main.listen_http 2>/dev/null || true)" saved_https="$(uci -q get uhttpd.main.listen_https 2>/dev/null || true)" uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true for address in $saved_http; do uci add_list "$CONFIG.main.uhttpd_saved_listen_http=$address" done for address in $saved_https; do uci add_list "$CONFIG.main.uhttpd_saved_listen_https=$address" done uci set "$CONFIG.main.uhttpd_bind_owned=1" uci commit "$CONFIG" } restore_uhttpd_bindings() { [ "$(uci_get main uhttpd_bind_owned 0)" = 1 ] || return 0 set_uhttpd_list listen_http "$(uci_get main uhttpd_saved_listen_http)" set_uhttpd_list listen_https "$(uci_get main uhttpd_saved_listen_https)" uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true uci -q delete "$CONFIG.main.uhttpd_bind_owned" || true uci commit "$CONFIG" }