343 lines
7.2 KiB
Bash
343 lines
7.2 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-}
|
|
local char code width=0
|
|
while IFS= read -r -n1 char; do
|
|
code=$(printf '%d' "'${char}")
|
|
if (( code >= 32 && code <= 126 )); then
|
|
((width += 1))
|
|
else
|
|
((width += 2))
|
|
fi
|
|
done < <(printf '%s' "${input}")
|
|
printf '%s\n' "${width}"
|
|
}
|
|
|
|
repeat_char() {
|
|
local char=$1
|
|
local count=$2
|
|
local out=''
|
|
while (( count > 0 )); do
|
|
out+="${char}"
|
|
((count--))
|
|
done
|
|
printf '%s' "${out}"
|
|
}
|
|
|
|
term_width() {
|
|
[[ -n ${COLUMNS:-} ]] && { printf '%s\n' "${COLUMNS}"; return; }
|
|
command_is_available tput && tput cols 2>/dev/null && return
|
|
printf '80\n'
|
|
}
|
|
|
|
use_box_ui() {
|
|
[[ ${IPF_FORCE_PLAIN_UI} != 1 && $(term_width) -ge 60 ]]
|
|
}
|
|
|
|
box_width() {
|
|
local width
|
|
width=$(term_width)
|
|
(( width > 78 )) && width=78
|
|
printf '%s\n' "${width}"
|
|
}
|
|
|
|
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_border() {
|
|
local left=$1 right=$2 width=${3:-60}
|
|
printf '%s%s%s\n' "${left}" "$(repeat_char '═' "$((width - 2))")" "${right}"
|
|
}
|
|
|
|
box_top() { box_border '╔' '╗' "${1:-60}"; }
|
|
box_bottom() { box_border '╚' '╝' "${1:-60}"; }
|
|
box_separator() { box_border '╠' '╣' "${1:-60}"; }
|
|
|
|
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-} width
|
|
if use_box_ui; then
|
|
width=$(box_width)
|
|
box_top "${width}"
|
|
box_line "${width}" "${title}"
|
|
box_bottom "${width}"
|
|
return
|
|
fi
|
|
printf '== %s ==\n' "${title}"
|
|
}
|
|
|
|
read_test_input() {
|
|
READ_TEST_INPUT_RESULT=''
|
|
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
|
if [[ ${IPF_TEST_INPUTS} == *$'\n'* ]]; then
|
|
READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS%%$'\n'*}
|
|
IPF_TEST_INPUTS=${IPF_TEST_INPUTS#*$'\n'}
|
|
else
|
|
READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS}
|
|
IPF_TEST_INPUTS=''
|
|
fi
|
|
fi
|
|
}
|
|
|
|
prompt_input() {
|
|
local prompt=$1
|
|
local default_value=${2-}
|
|
local reply=''
|
|
|
|
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
|
read_test_input
|
|
reply=${READ_TEST_INPUT_RESULT}
|
|
if [[ -z ${reply} && -n ${default_value} ]]; then
|
|
reply=${default_value}
|
|
fi
|
|
PROMPT_INPUT_RESULT=${reply}
|
|
printf '%s\n' "${reply}" >&2
|
|
printf '%s\n' "${reply}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n ${default_value} ]]; then
|
|
read -r -p "${prompt} [${default_value}]: " reply
|
|
[[ -n ${reply} ]] || reply=${default_value}
|
|
else
|
|
read -r -p "${prompt}: " reply
|
|
fi
|
|
PROMPT_INPUT_RESULT=${reply}
|
|
printf '%s\n' "${reply}"
|
|
}
|
|
|
|
prompt_input_capture() {
|
|
prompt_input "$@" >/dev/null
|
|
}
|
|
|
|
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
|
|
|
|
prompt_input_capture "${prompt} ${suffix}" ""
|
|
answer=${PROMPT_INPUT_RESULT}
|
|
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
|
|
prompt_input_capture "${prompt}" ""
|
|
answer=${PROMPT_INPUT_RESULT}
|
|
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= count )); then
|
|
PROMPT_SELECT_RESULT=$((answer - 1))
|
|
printf '%s\n' "${PROMPT_SELECT_RESULT}"
|
|
return 0
|
|
fi
|
|
log_warn "请输入 1-${count} 之间的编号。"
|
|
done
|
|
}
|
|
|
|
prompt_select_capture() {
|
|
prompt_select "$@" >/dev/null
|
|
}
|
|
|
|
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_part_count() {
|
|
local part=${1-}
|
|
local IFS=':'
|
|
local -a fields=()
|
|
local field idx count=0
|
|
|
|
[[ -n ${part} ]] || {
|
|
printf '0\n'
|
|
return 0
|
|
}
|
|
|
|
[[ ${part} != :* ]] || return 1
|
|
[[ ${part} != *: ]] || return 1
|
|
[[ ${part} != *::* ]] || return 1
|
|
|
|
read -r -a fields <<<"${part}"
|
|
for idx in "${!fields[@]}"; do
|
|
field=${fields[idx]}
|
|
[[ -n ${field} ]] || return 1
|
|
if [[ ${field} == IPV4TAIL ]]; then
|
|
(( idx == ${#fields[@]} - 1 )) || return 1
|
|
((count += 2))
|
|
continue
|
|
fi
|
|
[[ ${field} =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1
|
|
((count += 1))
|
|
done
|
|
|
|
printf '%s\n' "${count}"
|
|
}
|
|
|
|
validate_ipv6() {
|
|
local ip=${1-}
|
|
local base scope tail prefix left right left_count right_count total_count
|
|
[[ -n ${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
|
|
|
|
if [[ ${base} == *.* ]]; then
|
|
tail=${base##*:}
|
|
prefix=${base%:*}
|
|
[[ ${prefix} != "${base}" ]] || return 1
|
|
validate_ipv4 "${tail}" || return 1
|
|
base="${prefix}:IPV4TAIL"
|
|
fi
|
|
|
|
[[ ${base//IPV4TAIL/} =~ ^[0-9A-Fa-f:]+$ ]] || return 1
|
|
|
|
if [[ ${base} == *::* ]]; then
|
|
[[ ${base#*::} != *::* ]] || return 1
|
|
left=${base%%::*}
|
|
right=${base#*::}
|
|
left_count=$(_validate_ipv6_part_count "${left}") || return 1
|
|
right_count=$(_validate_ipv6_part_count "${right}") || return 1
|
|
total_count=$((left_count + right_count))
|
|
(( total_count < 8 )) || return 1
|
|
return 0
|
|
fi
|
|
|
|
total_count=$(_validate_ipv6_part_count "${base}") || return 1
|
|
(( total_count == 8 ))
|
|
}
|
|
|
|
validate_port() {
|
|
local port=${1-}
|
|
[[ ${port} =~ ^[0-9]+$ ]] || return 1
|
|
(( port >= 1 && port <= 65535 ))
|
|
}
|
|
|
|
validate_proto() {
|
|
[[ ${1-} =~ ^(tcp|udp|both)$ ]]
|
|
}
|
|
|
|
validate_ipver() {
|
|
[[ ${1-} =~ ^(4|6|both)$ ]]
|
|
}
|
|
|
|
require_root() {
|
|
if (( EUID != 0 )); then
|
|
log_err '请使用 root 或 sudo 运行此脚本。'
|
|
return 2
|
|
fi
|
|
}
|