Files
IPTables-Management/lib/common.sh

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
}