297 lines
5.8 KiB
Bash
297 lines
5.8 KiB
Bash
#!/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
|
|
}
|