#!/usr/bin/env bash # Общие функции для скриптов проекта WireGuard. if [[ -t 1 ]]; then C_RESET='\033[0m' C_RED='\033[0;31m' C_GREEN='\033[0;32m' C_YELLOW='\033[1;33m' C_BLUE='\033[0;34m' else C_RESET='' C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' fi LOG_FILE="${LOG_FILE:-}" timestamp() { date '+%Y-%m-%d %H:%M:%S' } _write_log() { local level="$1" local msg="$2" local line="[$(timestamp)] [$level] $msg" if [[ -n "${LOG_FILE}" ]]; then mkdir -p "$(dirname "$LOG_FILE")" printf '%s\n' "$line" >> "$LOG_FILE" fi } log_info() { local msg="$*" printf '%b[INFO]%b %s\n' "$C_BLUE" "$C_RESET" "$msg" _write_log "INFO" "$msg" } log_warn() { local msg="$*" printf '%b[WARN]%b %s\n' "$C_YELLOW" "$C_RESET" "$msg" _write_log "WARN" "$msg" } log_error() { local msg="$*" printf '%b[ERR ]%b %s\n' "$C_RED" "$C_RESET" "$msg" >&2 _write_log "ERROR" "$msg" } log_success() { local msg="$*" printf '%b[ OK ]%b %s\n' "$C_GREEN" "$C_RESET" "$msg" _write_log "SUCCESS" "$msg" } die() { log_error "$*" exit 1 } require_root() { if [[ "${EUID}" -ne 0 ]]; then die "Скрипт должен выполняться от root." fi } require_cmd() { local cmd="$1" command -v "$cmd" >/dev/null 2>&1 || die "Не найдена команда: $cmd" } check_os_supported() { if [[ ! -r /etc/os-release ]]; then die "Не найден файл /etc/os-release, определить ОС не удалось." fi # shellcheck disable=SC1091 source /etc/os-release local id="${ID:-}" local like="${ID_LIKE:-}" if [[ "$id" != "debian" && "$id" != "ubuntu" && "$like" != *"debian"* ]]; then die "Поддерживаются только Debian/Ubuntu. Обнаружено: ${PRETTY_NAME:-unknown}" fi } apt_install_if_missing() { local pkgs=() local pkg for pkg in "$@"; do if ! dpkg -s "$pkg" >/dev/null 2>&1; then pkgs+=("$pkg") fi done if ((${#pkgs[@]} == 0)); then log_info "Все необходимые пакеты уже установлены." return fi log_info "Устанавливаю пакеты: ${pkgs[*]}" export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get install -y "${pkgs[@]}" } backup_file() { local file="$1" if [[ -f "$file" ]]; then local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)" cp -a "$file" "$backup" log_info "Создана резервная копия: $backup" fi } ensure_line_in_file() { local line="$1" local file="$2" touch "$file" if ! grep -Fxq "$line" "$file"; then printf '%s\n' "$line" >> "$file" fi } set_kv_in_file() { local key="$1" local value="$2" local file="$3" touch "$file" if grep -Eq "^${key}=" "$file"; then sed -i "s|^${key}=.*|${key}=${value}|" "$file" else printf '%s=%s\n' "$key" "$value" >> "$file" fi } is_valid_port() { local p="$1" [[ "$p" =~ ^[0-9]+$ ]] || return 1 ((p >= 1 && p <= 65535)) } is_valid_cidr_list() { local input="$1" local cidr IFS=',' read -r -a _cidrs <<< "$input" for cidr in "${_cidrs[@]}"; do cidr="$(echo "$cidr" | xargs)" [[ -n "$cidr" ]] || return 1 if ! [[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[12][0-9]|3[0-2])$|^::/0$ ]]; then return 1 fi done } ask_with_default() { local prompt="$1" local default="$2" local var_name="$3" local answer read -r -p "$prompt [$default]: " answer answer="${answer:-$default}" printf -v "$var_name" '%s' "$answer" } ask_secret() { local prompt="$1" local var_name="$2" local answer read -r -s -p "$prompt: " answer echo printf -v "$var_name" '%s' "$answer" } confirm() { local prompt="$1" local ans read -r -p "$prompt [y/N]: " ans [[ "$ans" =~ ^([yY][eE][sS]|[yY])$ ]] } detect_public_ip() { local ip for url in "https://api.ipify.org" "https://ifconfig.me/ip" "https://icanhazip.com"; do if ip="$(curl -4fsS --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]')"; then if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then echo "$ip" return 0 fi fi done return 1 } detect_default_iface() { ip route show default 2>/dev/null | awk '{print $5; exit}' } detect_default_gateway() { ip route show default 2>/dev/null | awk '{print $3; exit}' } safe_chmod_600() { local file="$1" [[ -f "$file" ]] || return 0 chmod 600 "$file" } safe_chmod_700() { local dir="$1" [[ -d "$dir" ]] || return 0 chmod 700 "$dir" } systemd_enable_now() { local unit="$1" systemctl daemon-reload systemctl enable --now "$unit" } sanitize_name() { local input="$1" echo "$input" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_.-' | sed 's/^[-.]*//;s/[-.]*$//' } random_alnum() { local len="${1:-20}" local out="" while ((${#out} < len)); do # openssl выдает конечный блок данных, что безопасно для pipefail # и не приводит к SIGPIPE как в схеме с head/tr от /dev/urandom. out+="$( openssl rand -base64 48 2>/dev/null \ | tr -dc 'A-Za-z0-9' )" done printf '%s' "${out:0:len}" }