#!/usr/bin/env bash if [[ -n ${IPF_COMMON_SH_LOADED:-} ]]; then return 0 fi IPF_COMMON_SH_LOADED=1 : "${IPF_COLOR_ENABLED:=1}" : "${IPF_FORCE_PLAIN_UI:=0}" : "${IPF_APP_NAME:=IPTables 端口转发管理工具}" if [[ ! -t 1 ]]; then IPF_COLOR_ENABLED=0 fi if [[ ${IPF_COLOR_ENABLED} == 1 ]]; then CLR_RED=$'\033[31m' CLR_GREEN=$'\033[32m' CLR_YELLOW=$'\033[33m' CLR_BLUE=$'\033[34m' CLR_RESET=$'\033[0m' else CLR_RED='' CLR_GREEN='' CLR_YELLOW='' CLR_BLUE='' CLR_RESET='' fi command_is_available() { local cmd=${1:-} if [[ -z ${cmd} ]]; then return 1 fi if [[ ${cmd} == */* ]]; then [[ -x ${cmd} ]] return fi command -v "${cmd}" >/dev/null 2>&1 } ts_now() { date '+%H:%M:%S' } _log_with_color() { local color=$1 local level=$2 shift 2 printf '%s[%s] [%s]%s %s\n' "${color}" "$(ts_now)" "${level}" "${CLR_RESET}" "$*" >&2 } log_info() { _log_with_color "${CLR_BLUE}" INFO "$@"; } log_warn() { _log_with_color "${CLR_YELLOW}" WARN "$@"; } log_err() { _log_with_color "${CLR_RED}" ERROR "$@"; } log_ok() { _log_with_color "${CLR_GREEN}" OK "$@"; } display_width() { local input=${1-} awk -v str="${input}" 'BEGIN { n = split(str, chars, "") width = 0 for (i = 1; i <= n; i++) { if (chars[i] ~ /[ -~]/) { width += 1 } else { width += 2 } } print width }' } repeat_char() { local char=$1 local count=$2 local out='' while (( count > 0 )); do out+="${char}" ((count--)) done printf '%s' "${out}" } term_width() { if [[ -n ${COLUMNS:-} ]]; then printf '%s\n' "${COLUMNS}" return fi if command_is_available tput; then tput cols 2>/dev/null && return fi printf '80\n' } use_box_ui() { [[ ${IPF_FORCE_PLAIN_UI} != 1 ]] || return 1 [[ $(term_width) -ge 60 ]] } pad_right() { local text=${1-} local width=$2 local actual padding actual=$(display_width "${text}") if (( actual >= width )); then printf '%s' "${text}" return fi padding=$((width - actual)) printf '%s%*s' "${text}" "${padding}" '' } box_top() { local width=${1:-60} printf '╔%s╗\n' "$(repeat_char '═' "$((width - 2))")" } box_bottom() { local width=${1:-60} printf '╚%s╝\n' "$(repeat_char '═' "$((width - 2))")" } box_separator() { local width=${1:-60} printf '╠%s╣\n' "$(repeat_char '═' "$((width - 2))")" } box_line() { local width=${1:-60} local text=${2-} local inner=$((width - 4)) printf '║ %s ║\n' "$(pad_right "${text}" "${inner}")" } print_section() { local title=${1-} if use_box_ui; then local width width=$(term_width) if (( width > 78 )); then width=78 fi box_top "${width}" box_line "${width}" "${title}" box_bottom "${width}" else printf '== %s ==\n' "${title}" fi } read_test_input() { local reply='' if [[ -n ${IPF_TEST_INPUTS-} ]]; then if [[ ${IPF_TEST_INPUTS} == *$'\n'* ]]; then reply=${IPF_TEST_INPUTS%%$'\n'*} IPF_TEST_INPUTS=${IPF_TEST_INPUTS#*$'\n'} else reply=${IPF_TEST_INPUTS} IPF_TEST_INPUTS='' fi fi printf '%s' "${reply}" } prompt_input() { local prompt=$1 local default_value=${2-} local reply='' if [[ -n ${IPF_TEST_INPUTS-} ]]; then reply=$(read_test_input) if [[ -z ${reply} && -n ${default_value} ]]; then reply=${default_value} fi printf '%s\n' "${reply}" >&2 printf '%s\n' "${reply}" return 0 fi if [[ -n ${default_value} ]]; then read -r -p "${prompt} [${default_value}]: " reply reply=${reply:-${default_value}} else read -r -p "${prompt}: " reply fi printf '%s\n' "${reply}" } prompt_confirm() { local prompt=$1 local default_choice=${2:-n} local answer normalized suffix if [[ ${default_choice} == y ]]; then suffix='[Y/n]' else suffix='[y/N]' fi answer=$(prompt_input "${prompt} ${suffix}" "") if [[ -z ${answer} ]]; then answer=${default_choice} fi normalized=${answer,,} [[ ${normalized} == y || ${normalized} == yes ]] } prompt_select() { local prompt=$1 shift local options=("$@") local idx answer count count=${#options[@]} for ((idx = 0; idx < count; idx++)); do printf ' [%d] %s\n' "$((idx + 1))" "${options[idx]}" >&2 done while true; do answer=$(prompt_input "${prompt}" "") if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= count )); then printf '%s\n' "$((answer - 1))" return 0 fi log_warn "请输入 1-${count} 之间的编号。" done } pause_return() { if [[ -n ${IPF_TEST_INPUTS-} ]]; then return 0 fi read -r -p '按回车继续...' _ } validate_ipv4() { local ip=${1-} local IFS='.' local -a parts=() local part [[ ${ip} =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 read -r -a parts <<<"${ip}" for part in "${parts[@]}"; do [[ ${part} =~ ^[0-9]+$ ]] || return 1 (( part >= 0 && part <= 255 )) || return 1 done } validate_ipv6() { local ip=${1-} local base scope [[ -n ${ip} ]] || return 1 [[ ${ip} != *.* ]] || return 1 [[ ${ip} == *:* ]] || return 1 [[ ${ip} != *:::* ]] || return 1 scope='' base=${ip} if [[ ${ip} == *%* ]]; then scope=${ip#*%} base=${ip%%\%*} [[ -n ${scope} ]] || return 1 [[ ${scope} =~ ^[A-Za-z0-9_.:-]+$ ]] || return 1 fi [[ ${base} =~ ^[0-9A-Fa-f:]+$ ]] || return 1 [[ ${base} =~ :: ]] || [[ ${base} =~ ^([0-9A-Fa-f]{1,4}:){1,7}[0-9A-Fa-f]{1,4}$ ]] || return 1 } validate_port() { local port=${1-} [[ ${port} =~ ^[0-9]+$ ]] || return 1 (( port >= 1 && port <= 65535 )) } validate_proto() { case ${1-} in tcp|udp|both) return 0 ;; *) return 1 ;; esac } validate_ipver() { case ${1-} in 4|6|both) return 0 ;; *) return 1 ;; esac } require_root() { if (( EUID != 0 )); then log_err '请使用 root 或 sudo 运行此脚本。' return 2 fi }