Files
lemana-vpn/install.sh
T

1289 lines
42 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
APP_NAME="lemana-vpn"
DEFAULT_RAW_BASE_URL="https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main"
RAW_BASE_URL="${LEMANA_VPN_RAW_BASE_URL:-$DEFAULT_RAW_BASE_URL}"
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
# Read saved scalar settings without executing shell code. Explicit environment wins.
read_saved_settings() {
[ -f "$CONFIG_DIR/env" ] || [ -f "$OC_CONFIG_DIR/config.toml" ] || return 0
CONFIG_FILE="$CONFIG_DIR/env" OC_CONFIG_FILE="$OC_CONFIG_DIR/config.toml" python3 - <<'PYCONFIG'
import json
import os
import re
import shlex
from pathlib import Path
keys = ("MODE", "USERNAME", "BW_ITEM", "USE_TOUCHID", "AUTH_PROFILE", "CREDENTIAL_SOURCE", "USE_BITWARDEN", "LOCAL_OTP", "AUTO_SOURCE")
pattern = re.compile(r"(?m)^(?:export[ \t]+)?(LEMANA_VPN_(?:" + "|".join(keys) + r"))=[ \t]*(\"(?:\\[\s\S]|[^\"\\])*\"|'[^']*'|[^\r\n]*)[ \t]*(?:#.*)?$")
values = {}
path = Path(os.environ["CONFIG_FILE"])
if path.exists():
for match in pattern.finditer(path.read_text()):
key, raw = match.groups()
if raw.startswith('"'):
value = re.sub(r'\\([$`"\\\n])', lambda m: "" if m[1] == "\n" else m[1], raw[1:-1])
else:
parts = shlex.split(raw, comments=True)
if len(parts) > 1:
continue
value = parts[0] if parts else ""
values[key] = value
if not values.get("LEMANA_VPN_USERNAME") and "LEMANA_VPN_USERNAME" not in os.environ:
path = Path(os.environ["OC_CONFIG_FILE"])
if path.exists():
section = re.search(r"""(?ms)^[ \t]*\[[ \t]*(?:credentials|"credentials"|'credentials')[ \t]*\][ \t]*(?:#[^\n]*)?(?:\n|\Z)(.*?)(?=^[ \t]*\[|\Z)""", path.read_text())
match = re.search(r"""(?m)^[ \t]*(?:username|"username"|'username')[ \t]*=[ \t]*("(?:\\.|[^"\\])*"|'[^']*')""", section[1] if section else "")
if match:
raw = match[1]
values["LEMANA_VPN_USERNAME"] = json.loads(raw) if raw.startswith('"') else raw[1:-1]
# Select auth behavior from one priority layer; saved legacy fields cannot override it.
if "LEMANA_VPN_MODE" not in os.environ and any("LEMANA_VPN_" + key in os.environ for key in ("AUTH_PROFILE", "CREDENTIAL_SOURCE", "USE_BITWARDEN")):
for key in ("MODE", "AUTH_PROFILE", "CREDENTIAL_SOURCE", "USE_BITWARDEN", "LOCAL_OTP"):
values.pop("LEMANA_VPN_" + key, None)
if "LEMANA_VPN_MODE" in os.environ:
values.pop("LEMANA_VPN_LOCAL_OTP", None)
for key, value in values.items():
if key not in os.environ:
print(key + "=" + shlex.quote(value))
if key == "LEMANA_VPN_USERNAME":
print("SAVED_USERNAME=" + shlex.quote(value))
PYCONFIG
}
saved_settings="$(read_saved_settings)"
eval "$saved_settings"
USERNAME="${LEMANA_VPN_USERNAME:-}"
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
MODE="${LEMANA_VPN_MODE:-}"
LOCAL_OTP="${LEMANA_VPN_LOCAL_OTP:-0}"
AUTO_SOURCE="${LEMANA_VPN_AUTO_SOURCE:-keychain}"
MANUAL_FORCED=0
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE:-manual}"
USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-0}"
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-0}"
INSTALL_SUDOERS=1
INSTALL_ALIASES=1
INSTALL_APP=1
INSTALL_AUTOSTART=1
CONFIGURE_LOGIN=0
CONFIGURE_KEYCHAIN=0
IMPORT_YANDEX_KEY=0
DRY_RUN=0
FORCE=0
INTERACTIVE=auto
BITWARDEN_FORCED=0
MODE_FORCED=0
CREDENTIAL_SOURCE_FORCED=0
AUTH_PROFILE_FORCED=0
TOUCHID_FORCED=0
[ "${LEMANA_VPN_USE_TOUCHID+x}" != x ] || TOUCHID_FORCED=1
SUDOERS_FORCED=0
SHELL_FORCED=0
APP_FORCED=0
AUTOSTART_FORCED=0
CONFIGURE_KEYCHAIN_FORCED=0
CONFIGURE_LOGIN_FORCED=0
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
C_RESET="$(printf '\033[0m')"
C_BOLD="$(printf '\033[1m')"
C_DIM="$(printf '\033[2m')"
C_RED="$(printf '\033[31m')"
C_GREEN="$(printf '\033[32m')"
C_YELLOW="$(printf '\033[33m')"
C_BLUE="$(printf '\033[34m')"
C_CYAN="$(printf '\033[36m')"
else
C_RESET=""
C_BOLD=""
C_DIM=""
C_RED=""
C_GREEN=""
C_YELLOW=""
C_BLUE=""
C_CYAN=""
fi
if [ "${LEMANA_VPN_NO_EMOJI:-0}" = "1" ]; then
E_STEP=">"
E_INFO="i"
E_OK="+"
E_WARN="!"
E_SKIP="-"
E_ERROR="x"
else
E_STEP="➡️"
E_INFO="ℹ️"
E_OK="✅"
E_WARN="⚠️"
E_SKIP="⏭️"
E_ERROR="❌"
fi
usage() {
cat <<'USAGE'
Usage:
sh install.sh [options]
Options:
--with-bitwarden Install/use Bitwarden CLI module as an optional credential source
--without-bitwarden Do not install/use Bitwarden CLI (default)
--mode VALUE Profile: manual-keychain, keychain-auto or bitwarden-auto
--credential-source VALUE Legacy credential source: none, keychain, or bitwarden
--auth-profile VALUE Legacy compatibility profile
--with-touchid Install/use keychain-fingerprint Touch ID helper with Bitwarden
--without-touchid Do not install/use Touch ID helper
--configure-login Prompt for LDAP username/password after install
--configure-keychain Prompt for LDAP password and permanent 2FA setup code after install
--import-yandex-key Import a local Yandex Key backup after login setup (Terminal required)
--username VALUE Corporate LDAP username (otherwise asked on first Terminal connection)
--bw-item VALUE Bitwarden item name (default: LM LDAP)
--raw-base-url URL Raw file base URL for curl installs
--no-sudoers Do not install sudoers rules
--no-shell Do not update ~/.zshrc aliases
--with-app Build/install macOS menu bar app (default)
--without-app Do not build/install macOS menu bar app
--with-autostart Install LaunchAgent for menu bar app (default)
--without-autostart Do not install LaunchAgent
--interactive Ask before installing optional missing modules
--non-interactive Use selected/default modules without prompts
--minimal Manual-first install without optional credential helpers
--dry-run Print actions without changing files
--force Reinstall files even when present
-h, --help Show this help
Examples:
sh install.sh
sh install.sh --mode manual-keychain --configure-login
sh install.sh --mode bitwarden-auto
sh install.sh --configure-login
sh install.sh --without-touchid
USAGE
}
apply_mode_defaults() {
case "$MODE" in
manual-keychain)
LOCAL_OTP=0
AUTH_PROFILE="saved-login"
CREDENTIAL_SOURCE="keychain"
USE_BITWARDEN=0
USE_TOUCHID=0
;;
keychain-auto)
AUTH_PROFILE="auto"
CREDENTIAL_SOURCE="keychain"
USE_BITWARDEN=0
USE_TOUCHID=0
LOCAL_OTP=1
AUTO_SOURCE=keychain
;;
bitwarden-auto)
LOCAL_OTP=0
AUTO_SOURCE=bitwarden
AUTH_PROFILE="auto"
CREDENTIAL_SOURCE="bitwarden"
USE_BITWARDEN=1
if [ "$TOUCHID_FORCED" -eq 0 ]; then
USE_TOUCHID=1
fi
;;
*)
echo "--mode requires manual-keychain, keychain-auto or bitwarden-auto" >&2
exit 1
;;
esac
}
normalize_mode_from_legacy() {
if [ -z "$MODE" ]; then
[ -n "$CREDENTIAL_SOURCE" ] || { [ "$USE_BITWARDEN" -ne 1 ] || CREDENTIAL_SOURCE=bitwarden; }
case "$CREDENTIAL_SOURCE:$AUTH_PROFILE" in
bitwarden:*|*:auto)
MODE="bitwarden-auto"
;;
*)
MODE="manual-keychain"
;;
esac
fi
if [ "$MODE" = manual-keychain ] && [ "$LOCAL_OTP" = 1 ] && [ "$MANUAL_FORCED" -eq 0 ]; then
MODE=keychain-auto
fi
apply_mode_defaults
}
while [ "$#" -gt 0 ]; do
case "$1" in
--with-bitwarden)
MODE="bitwarden-auto"
MODE_FORCED=1
apply_mode_defaults
BITWARDEN_FORCED=1
CREDENTIAL_SOURCE_FORCED=1
;;
--without-bitwarden)
MANUAL_FORCED=1
MODE="manual-keychain"
MODE_FORCED=1
apply_mode_defaults
USE_BITWARDEN=0
USE_TOUCHID=0
BITWARDEN_FORCED=1
;;
--mode)
shift
[ "$#" -gt 0 ] || { echo "--mode requires manual-keychain, keychain-auto or bitwarden-auto" >&2; exit 1; }
MODE="$1"
[ "$MODE" != manual-keychain ] || MANUAL_FORCED=1
MODE_FORCED=1
apply_mode_defaults
;;
--credential-source)
shift
[ "$#" -gt 0 ] || { echo "--credential-source requires none, keychain, or bitwarden" >&2; exit 1; }
CREDENTIAL_SOURCE="$1"
CREDENTIAL_SOURCE_FORCED=1
case "$CREDENTIAL_SOURCE" in
none|keychain)
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="manual-keychain"
MANUAL_FORCED=1
fi
;;
bitwarden)
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="bitwarden-auto"
fi
;;
*)
echo "--credential-source requires none, keychain, or bitwarden" >&2
exit 1
;;
esac
;;
--auth-profile)
shift
[ "$#" -gt 0 ] || { echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2; exit 1; }
AUTH_PROFILE="$1"
AUTH_PROFILE_FORCED=1
case "$AUTH_PROFILE" in
manual|saved-login)
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="manual-keychain"
MANUAL_FORCED=1
fi
;;
saved-totp|auto)
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="bitwarden-auto"
fi
;;
*)
echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2
exit 1
;;
esac
;;
--with-touchid)
USE_TOUCHID=1
TOUCHID_FORCED=1
;;
--without-touchid)
USE_TOUCHID=0
TOUCHID_FORCED=1
;;
--configure-login)
MANUAL_FORCED=1
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="manual-keychain"
fi
CONFIGURE_LOGIN=1
CONFIGURE_LOGIN_FORCED=1
CONFIGURE_KEYCHAIN=0
;;
--import-yandex-key)
IMPORT_YANDEX_KEY=1
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="keychain-auto"
fi
MODE_FORCED=1
;;
--configure-keychain)
if [ "$MODE_FORCED" -eq 0 ]; then
MODE="bitwarden-auto"
fi
CONFIGURE_KEYCHAIN=1
CONFIGURE_LOGIN=0
CONFIGURE_KEYCHAIN_FORCED=1
;;
--username)
shift
[ "$#" -gt 0 ] || { echo "--username requires a value" >&2; exit 1; }
USERNAME="$1"
;;
--bw-item)
shift
[ "$#" -gt 0 ] || { echo "--bw-item requires a value" >&2; exit 1; }
BW_ITEM="$1"
;;
--raw-base-url)
shift
[ "$#" -gt 0 ] || { echo "--raw-base-url requires a value" >&2; exit 1; }
RAW_BASE_URL="${1%/}"
;;
--no-sudoers)
INSTALL_SUDOERS=0
SUDOERS_FORCED=1
;;
--no-shell)
INSTALL_ALIASES=0
SHELL_FORCED=1
;;
--with-app)
INSTALL_APP=1
APP_FORCED=1
;;
--without-app)
INSTALL_APP=0
INSTALL_AUTOSTART=0
APP_FORCED=1
AUTOSTART_FORCED=1
;;
--with-autostart)
INSTALL_AUTOSTART=1
AUTOSTART_FORCED=1
;;
--without-autostart)
INSTALL_AUTOSTART=0
AUTOSTART_FORCED=1
;;
--interactive) INTERACTIVE=1 ;;
--non-interactive) INTERACTIVE=0 ;;
--minimal)
MANUAL_FORCED=1
MODE="manual-keychain"
MODE_FORCED=1
apply_mode_defaults
BITWARDEN_FORCED=1
CREDENTIAL_SOURCE_FORCED=1
AUTH_PROFILE_FORCED=1
TOUCHID_FORCED=1
;;
--dry-run) DRY_RUN=1 ;;
--force) FORCE=1 ;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
[ -z "$MODE" ] || MODE_FORCED=1
normalize_mode_from_legacy
log() {
printf '%s\n' "$*"
}
color_line() {
color="$1"
shift
printf '%s%s%s\n' "$color" "$*" "$C_RESET"
}
log_step() {
color_line "$C_BOLD$C_CYAN" "$E_STEP $*"
}
log_info() {
color_line "$C_BLUE" "$E_INFO $*"
}
log_detail() {
color_line "$C_DIM" " $*"
}
log_ok() {
color_line "$C_GREEN" "$E_OK $*"
}
log_warn() {
color_line "$C_YELLOW" "$E_WARN $*"
}
log_skip() {
color_line "$C_DIM" "$E_SKIP $*"
}
die() {
printf '%s%s ERROR: %s%s\n' "$C_RED" "$E_ERROR" "$*" "$C_RESET" >&2
exit 1
}
run() {
if [ "$DRY_RUN" -eq 1 ]; then
printf '+'
for arg in "$@"; do
printf ' %s' "$arg"
done
printf '\n'
return 0
fi
"$@"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Command not found: $1"
}
tty_available() {
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } || return 1
{ : <> /dev/tty; } 2>/dev/null
}
has_tty() {
tty_available || [ -t 0 ]
}
interactive_enabled() {
case "$INTERACTIVE" in
1) has_tty ;;
0) return 1 ;;
auto) has_tty ;;
*) return 1 ;;
esac
}
prompt_printf() {
if tty_available; then
printf "$@" > /dev/tty
else
printf "$@"
fi
}
prompt_read_answer() {
if tty_available; then
IFS= read -r answer < /dev/tty || answer=""
else
IFS= read -r answer || answer=""
fi
}
preserve_existing_username() {
if [ -n "${SAVED_USERNAME:-}" ] && [ "$USERNAME" = "$SAVED_USERNAME" ]; then
log_info "Preserving existing LDAP username from previous config"
fi
}
yes_no() {
prompt="$1"
default_answer="$2"
[ "$default_answer" = "y" ] || [ "$default_answer" = "n" ] || die "Invalid yes_no default: $default_answer"
if ! interactive_enabled; then
[ "$default_answer" = "y" ]
return $?
fi
if [ "$default_answer" = "y" ]; then
suffix="[Y/n]"
else
suffix="[y/N]"
fi
while :; do
prompt_printf '%s%s %s%s ' "$C_BOLD" "$prompt" "$suffix" "$C_RESET"
prompt_read_answer
case "$answer" in
"") [ "$default_answer" = "y" ]; return $? ;;
y|Y|yes|YES|Yes|д|Д|да|Да|ДА) return 0 ;;
n|N|no|NO|No|н|Н|нет|Нет|НЕТ) return 1 ;;
*) prompt_printf 'Введите y или n.\n' ;;
esac
done
}
print_install_intro() {
log_step "Lemana VPN установит приложение для подключения к корпоративному VPN"
log_detail "Есть два режима: ручной ввод кода и автоматический вход. Для автоматического входа выберите ключ на Mac или Bitwarden."
log_detail "Ключ из Яндекс Ключа можно импортировать отдельно, без Bitwarden."
}
choose_auth_profile_interactive() {
log_step "Выбираю режим подключения"
log_detail "Если не уверены, просто нажмите Enter: будет ручной режим с сохранением логина и пароля."
while :; do
prompt_printf '\n'
prompt_printf '%sКакой режим использовать?%s\n' "$C_BOLD" "$C_RESET"
prompt_printf ' 1) Ручной вход: запомнить логин и пароль на этом Mac (рекомендуется)\n'
prompt_printf ' Логин и пароль подставятся из macOS Keychain. Одноразовый код вводится руками каждый раз.\n\n'
prompt_printf ' 2) Автоматический вход\n'
prompt_printf ' Пароль и код подставляются автоматически, форма отправляется без ручного ввода.\n'
prompt_printf '%sВыбор [1/2, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
prompt_read_answer
case "$answer" in
""|1)
MANUAL_FORCED=1
MODE=manual-keychain
apply_mode_defaults
CONFIGURE_LOGIN=1
CONFIGURE_KEYCHAIN=0
;;
2)
prompt_printf 'Источник данных: 1) Ключ на этом Mac, 2) Bitwarden [1/2, Enter=1]: '
prompt_read_answer
case "$answer" in
""|1) MODE=keychain-auto; CONFIGURE_LOGIN=1 ;;
2) MODE=bitwarden-auto; CONFIGURE_LOGIN=0 ;;
*) prompt_printf 'Введите 1 или 2.\n'; continue ;;
esac
apply_mode_defaults
CONFIGURE_KEYCHAIN=0
;;
*)
prompt_printf 'Введите 1 или 2. Если не уверены, нажмите Enter.\n'
continue
;;
esac
break
done
case "$MODE" in
manual-keychain)
log_info "Режим подключения: логин и пароль в macOS Keychain, одноразовый код вручную"
;;
keychain-auto)
log_info "Режим подключения: автоматический, ключ на этом Mac"
;;
bitwarden-auto)
log_info "Режим подключения: автоматический, Bitwarden"
;;
esac
}
bool_word() {
if "$@" >/dev/null 2>&1; then
printf 'yes'
else
printf 'no'
fi
}
keychain_has() {
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
}
zsh_aliases_installed() {
[ -f "$HOME/.zshrc" ] && grep -q '^# >>> lemana-vpn$' "$HOME/.zshrc"
}
print_detected_state() {
log_step "Проверяю установленное окружение"
log_detail "Так установщик понимает, какие модули уже есть, а по каким нужно спросить."
log "Detected state:"
log " openconnect: $(bool_word command -v openconnect)"
log " pipx: $(bool_word command -v pipx)"
log " openconnect-lite: $(bool_word test -x "$HOME/.local/bin/openconnect-lite")"
log " Bitwarden CLI: $(bool_word command -v bw)"
log " Touch ID helper: $(bool_word test -x "$INSTALL_BIN_DIR/keychain-fingerprint")"
log " DNS cleanup: $(bool_word test -x "$DNS_CLEANUP")"
log " sudoers: $(bool_word test -f /etc/sudoers.d/lemana-vpn-openconnect)/$(bool_word test -f /etc/sudoers.d/lemana-vpn-dns)"
log " shell aliases: $(bool_word zsh_aliases_installed)"
log " Swift: $(bool_word command -v swift)"
log " Menu Bar app: $(bool_word test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
log " LaunchAgent: $(bool_word test -f "$LAUNCH_AGENT")"
log " Keychain password: $(bool_word keychain_has openconnect-lite "$USERNAME")"
log " Keychain 2FA setup: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
}
choose_modules() {
if interactive_enabled; then
print_install_intro
fi
print_detected_state
case "$AUTH_PROFILE" in
manual|saved-login|saved-totp|auto) ;;
*)
die "Unknown auth profile: $AUTH_PROFILE"
;;
esac
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "none" ] && [ "$AUTH_PROFILE" != "manual" ]; then
CREDENTIAL_SOURCE=keychain
fi
case "$CREDENTIAL_SOURCE" in
none)
USE_BITWARDEN=0
USE_TOUCHID=0
;;
bitwarden)
USE_BITWARDEN=1
;;
keychain)
USE_BITWARDEN=0
USE_TOUCHID=0
;;
*)
die "Unknown credential source: $CREDENTIAL_SOURCE"
;;
esac
if ! interactive_enabled; then
log_skip "Interactive prompts: off"
return 0
fi
log_info "Interactive prompts: on"
log_detail "Установщик проведёт по понятному выбору режима входа; флаги командной строки имеют приоритет."
if [ "$MODE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
choose_auth_profile_interactive
fi
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
if yes_no "Bitwarden не найден на этом Mac. Установить его сейчас?" n; then
MODE=bitwarden-auto
apply_mode_defaults
else
MODE=manual-keychain
apply_mode_defaults
CONFIGURE_LOGIN=1
fi
fi
if [ "$TOUCHID_FORCED" -eq 0 ]; then
if [ "$USE_BITWARDEN" -eq 1 ]; then
if ! [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ]; then
if yes_no "Разрешить Touch ID для открытия Bitwarden?" y; then
USE_TOUCHID=1
else
USE_TOUCHID=0
fi
fi
else
USE_TOUCHID=0
fi
fi
if [ "$SUDOERS_FORCED" -eq 0 ]; then
if ! [ -f /etc/sudoers.d/lemana-vpn-openconnect ] || ! [ -f /etc/sudoers.d/lemana-vpn-dns ]; then
if yes_no "Настроить sudoers для VPN/DNS без повторного sudo-пароля?" y; then
INSTALL_SUDOERS=1
else
INSTALL_SUDOERS=0
fi
fi
fi
if [ "$SHELL_FORCED" -eq 0 ] && ! zsh_aliases_installed; then
if yes_no "Добавить алиасы vpn/vpn-debug/vpn-fix-dns в ~/.zshrc?" y; then
INSTALL_ALIASES=1
else
INSTALL_ALIASES=0
fi
fi
if [ "$APP_FORCED" -eq 0 ] && ! [ -x "$APP_DIR/Contents/MacOS/LemanaVPN" ]; then
if yes_no "Swift Menu Bar app не найден. Собрать и установить LemanaVPN.app?" y; then
INSTALL_APP=1
else
INSTALL_APP=0
INSTALL_AUTOSTART=0
fi
fi
if [ "$AUTOSTART_FORCED" -eq 0 ] && [ "$INSTALL_APP" -eq 1 ] && ! [ -f "$LAUNCH_AGENT" ]; then
if yes_no "Включить автозапуск LemanaVPN.app при логине?" y; then
INSTALL_AUTOSTART=1
else
INSTALL_AUTOSTART=0
fi
fi
if [ "$CONFIGURE_LOGIN_FORCED" -eq 0 ] && [ "$MODE" != "bitwarden-auto" ] && [ "$CONFIGURE_LOGIN" -eq 0 ]; then
if ! keychain_has openconnect-lite "$USERNAME"; then
if yes_no "Запомнить логин и пароль в macOS Keychain после установки?" y; then
CONFIGURE_LOGIN=1
fi
fi
fi
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$MODE" = "bitwarden-auto" ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
if yes_no "Сохранить пароль и постоянный код настройки 2FA в macOS Keychain после установки?" y; then
CONFIGURE_KEYCHAIN=1
fi
fi
fi
if [ "$MODE" != "bitwarden-auto" ] && [ "$IMPORT_YANDEX_KEY" -eq 0 ] && ! keychain_has openconnect-lite "totp/$USERNAME"; then
import_default=n
[ "$MODE" != keychain-auto ] || import_default=y
if yes_no "Импортировать код из Яндекс Ключа и включить автоматический вход?" "$import_default"; then
IMPORT_YANDEX_KEY=1
fi
fi
}
script_dir() {
case "$0" in
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
*)
if [ -f "$PWD/$0" ]; then
pwd
else
return 1
fi
;;
esac
}
download_file() {
src="$1"
dst="$2"
local_dir="$(script_dir 2>/dev/null || true)"
if [ -n "$local_dir" ] && [ -f "$local_dir/$src" ]; then
run cp "$local_dir/$src" "$dst"
return 0
fi
if [ "${LEMANA_VPN_USE_LOCAL_FILES:-0}" = "1" ] && [ -f "$PWD/$src" ]; then
run cp "$PWD/$src" "$dst"
return 0
fi
need_cmd curl
run curl -fsSL "$RAW_BASE_URL/$src" -o "$dst"
}
write_file() {
dst="$1"
content="$2"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ write %s\n' "$dst"
return 0
fi
printf '%s\n' "$content" > "$dst"
}
render_template_file() {
src="$1"
dst="$2"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ render %s -> %s\n' "$src" "$dst"
return 0
fi
USERNAME="$USERNAME" BW_ITEM="$BW_ITEM" DNS_CLEANUP="$DNS_CLEANUP" USE_TOUCHID="$USE_TOUCHID" AUTO_SOURCE="$AUTO_SOURCE" \
CONFIG_FILE="$CONFIG_DIR/env" python3 - "$src" "$dst" <<'PYCONFIG'
import os
import re
import sys
from pathlib import Path
def shell_value(value):
return value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`")
rendered = Path(sys.argv[1]).read_text()
rendered = re.sub(r"\{\{(USERNAME|BW_ITEM|DNS_CLEANUP|USE_TOUCHID|AUTO_SOURCE)\}\}", lambda m: shell_value(os.environ[m[1]]), rendered)
keys = re.findall(r"(?m)^(LEMANA_VPN_\w+)=", rendered)
existing = Path(os.environ["CONFIG_FILE"])
kept = ""
if existing.exists():
pattern = r"(?m)^(?:export[ \t]+)?(?:" + "|".join(keys) + r")=[ \t]*(?:\"(?:\\[\s\S]|[^\"\\])*\"|'[^']*'|[^\r\n]*)[ \t]*\n?"
kept = re.sub(pattern, "", existing.read_text()).rstrip()
Path(sys.argv[2]).write_text((kept + "\n" if kept else "") + rendered)
PYCONFIG
}
install_homebrew_packages() {
need_cmd brew
log_step "Проверяю Homebrew-зависимости"
log_detail "openconnect нужен как VPN-клиент, pipx — для изолированной установки openconnect-lite."
for pkg in openconnect pipx; do
if brew list "$pkg" >/dev/null 2>&1; then
log_ok "Homebrew package already installed: $pkg"
else
log_info "Installing Homebrew package: $pkg"
run brew install "$pkg"
fi
done
if [ "$USE_BITWARDEN" -eq 1 ]; then
if brew list bitwarden-cli >/dev/null 2>&1; then
log_ok "Homebrew package already installed: bitwarden-cli"
else
log_info "Installing Homebrew package: bitwarden-cli"
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и постоянный код настройки 2FA из item '$BW_ITEM'."
run brew install bitwarden-cli
fi
else
log_skip "Bitwarden module disabled; пропускаю bitwarden-cli."
fi
}
install_openconnect_lite() {
need_cmd pipx
log_step "Проверяю openconnect-lite"
log_detail "openconnect-lite открывает SAML/Keycloak SSO и передаёт полученную сессию в openconnect."
if [ -x "$HOME/.local/bin/openconnect-lite" ] && [ "$FORCE" -eq 0 ]; then
log_ok "openconnect-lite already installed"
else
log_info "Installing openconnect-lite via pipx"
run pipx install openconnect-lite
fi
if pipx --help 2>/dev/null | grep -q ' pin '; then
log_detail "Закрепляю pipx package, чтобы случайное обновление не сломало SSO-патчи."
run pipx pin openconnect-lite >/dev/null 2>&1 || true
fi
}
install_cli() {
tmp="$1"
log_step "Обновляю CLI и uninstall helper"
log_detail "CLI управляет подключением, статусом модулей, Bitwarden/Keychain sync и runtime-патчами."
run mkdir -p "$INSTALL_BIN_DIR"
download_file "bin/vpn-lemanapro.sh" "$tmp/vpn-lemanapro.sh"
run install -m 755 "$tmp/vpn-lemanapro.sh" "$INSTALL_BIN_DIR/vpn-lemanapro.sh"
log_ok "CLI installed: $INSTALL_BIN_DIR/vpn-lemanapro.sh"
download_file "bin/lemana-vpn-import-yandex.py" "$tmp/lemana-vpn-import-yandex.py"
run install -m 644 "$tmp/lemana-vpn-import-yandex.py" "$INSTALL_BIN_DIR/lemana-vpn-import-yandex.py"
download_file "uninstall.sh" "$tmp/uninstall.sh"
run install -m 755 "$tmp/uninstall.sh" "$INSTALL_BIN_DIR/uninstall-lemana-vpn.sh"
log_ok "Uninstall helper installed: $INSTALL_BIN_DIR/uninstall-lemana-vpn.sh"
}
install_config() {
tmp="$1"
mode_template="$tmp/$MODE.env"
log_step "Записываю конфигурацию"
log_detail "Здесь сохраняются выбранный режим входа, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
download_file "templates/openconnect-lite-config.toml" "$tmp/openconnect-lite-config.toml"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ render %s/config.toml\n' "$OC_CONFIG_DIR"
else
USERNAME="$USERNAME" python3 - "$OC_CONFIG_DIR/config.toml" "$tmp/openconnect-lite-config.toml" "$tmp/config.toml" <<'PYCONFIG'
import json
import os
import re
import sys
from pathlib import Path
existing, template, destination = map(Path, sys.argv[1:])
text = (existing if existing.exists() else template).read_text()
username = "username = " + json.dumps(os.environ["USERNAME"], ensure_ascii=False)
section = re.search(r"""(?ms)^[ \t]*\[[ \t]*(?:credentials|"credentials"|'credentials')[ \t]*\][ \t]*(?:#[^\n]*)?(?:\n|\Z)(.*?)(?=^[ \t]*\[|\Z)""", text)
if section:
body = section[1]
username_key = r"""(?m)^[ \t]*(?:username|"username"|'username')[ \t]*="""
if re.search(username_key, body):
body = re.sub(username_key + r".*$", lambda _: username, body)
else:
body = username + "\n" + body
prefix = text[:section.start(1)]
text = prefix + ("" if prefix.endswith("\n") else "\n") + body + text[section.end(1):]
else:
text = text.rstrip() + "\n\n[credentials]\n" + username + "\n"
destination.write_text(text)
PYCONFIG
fi
download_file "templates/$MODE.env" "$mode_template"
render_template_file "$mode_template" "$tmp/env"
run install -m 600 "$tmp/config.toml" "$OC_CONFIG_DIR/config.toml"
run install -m 600 "$tmp/env" "$CONFIG_DIR/env"
log_ok "Config installed: $CONFIG_DIR/env"
}
install_dns_cleanup() {
tmp="$1"
dns_cleanup_dir="$(dirname "$DNS_CLEANUP")"
log_step "Устанавливаю DNS cleanup wrapper"
log_detail "Wrapper сохраняет прежние DNS и восстанавливает изменения VPN после отключения или перезагрузки."
log_detail "macOS может запросить sudo-пароль для записи в $dns_cleanup_dir."
download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup"
run sudo install -d -m 755 -o root -g wheel "$dns_cleanup_dir"
log_info "Installing DNS cleanup wrapper: $DNS_CLEANUP"
run sudo install -m 755 -o root -g wheel "$tmp/lemana-vpn-dns-cleanup" "$DNS_CLEANUP"
log_ok "DNS cleanup wrapper installed"
}
install_sudoers() {
if [ "$INSTALL_SUDOERS" -ne 1 ]; then
log_skip "Sudoers module disabled; openconnect/DNS cleanup могут спрашивать пароль sudo."
return 0
fi
if [ -f /etc/sudoers.d/lemana-vpn-openconnect ] && [ -f /etc/sudoers.d/lemana-vpn-dns ] && [ "$FORCE" -eq 0 ]; then
log_ok "Sudoers rules already installed"
return 0
fi
log_step "Настраиваю sudoers"
log_detail "Это убирает повторный sudo-пароль при подключении, но разрешает только openconnect и DNS cleanup wrapper."
openconnect_bin="$(brew --prefix)/bin/openconnect"
[ -x "$openconnect_bin" ] || openconnect_bin="$(command -v openconnect || true)"
[ -n "$openconnect_bin" ] || die "openconnect binary not found"
tmp="$1"
current_user="$(id -un)"
run sudo install -d -m 755 -o root -g wheel /etc/sudoers.d
write_file "$tmp/sudoers-openconnect" "$current_user ALL=(ALL) NOPASSWD: $openconnect_bin"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-openconnect" /etc/sudoers.d/lemana-vpn-openconnect
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-openconnect
log_ok "sudoers openconnect rule is valid"
write_file "$tmp/sudoers-dns" "$current_user ALL=(ALL) NOPASSWD: $DNS_CLEANUP"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-dns" /etc/sudoers.d/lemana-vpn-dns
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-dns
log_ok "sudoers DNS cleanup rule is valid"
}
install_touchid_helper() {
if [ "$USE_TOUCHID" -ne 1 ]; then
log_skip "Touch ID module disabled; мастер-пароль Bitwarden будет вводиться вручную при необходимости."
return 0
fi
log_step "Проверяю Touch ID helper"
log_detail "Helper добавляет локальный Touch ID prompt перед чтением мастер-пароля Bitwarden из Keychain."
if [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ] && [ "$FORCE" -eq 0 ]; then
log_ok "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint"
return 0
fi
need_cmd git
need_cmd swiftc
tmp="$1"
log_info "Building keychain-fingerprint helper"
run git clone --depth 1 https://github.com/dss99911/keychain-fingerprint.git "$tmp/keychain-fingerprint"
run swiftc -o "$tmp/keychain-fingerprint-bin" "$tmp/keychain-fingerprint/main.swift" -framework LocalAuthentication -framework Security
run install -m 700 "$tmp/keychain-fingerprint-bin" "$INSTALL_BIN_DIR/keychain-fingerprint"
log_ok "Touch ID helper installed"
}
install_menu_bar_app() {
if [ "$INSTALL_APP" -ne 1 ]; then
log_skip "Menu Bar app disabled; CLI и алиасы останутся основным интерфейсом."
return 0
fi
need_cmd swift
tmp="$1"
app_src="$tmp/app"
log_step "Собираю Swift Menu Bar app"
log_detail "Приложение живёт в status bar и вызывает $INSTALL_BIN_DIR/vpn-lemanapro.sh для подключения и статуса модулей."
log_detail "Swift build может занять минуту; строки вида '[2/5] Write swift-version...' — нормальный вывод компилятора."
run mkdir -p "$app_src/Sources/LemanaVPN"
download_file "app/Package.swift" "$app_src/Package.swift"
download_file "app/Sources/LemanaVPN/LemanaVPNApp.swift" "$app_src/Sources/LemanaVPN/LemanaVPNApp.swift"
download_file "app/Sources/LemanaVPN/VPNManager.swift" "$app_src/Sources/LemanaVPN/VPNManager.swift"
download_file "app/Sources/LemanaVPN/BoundedLog.swift" "$app_src/Sources/LemanaVPN/BoundedLog.swift"
download_file "app/Sources/LemanaVPN/SessionTiming.swift" "$app_src/Sources/LemanaVPN/SessionTiming.swift"
download_file "app/Sources/LemanaVPN/VPNIcons.swift" "$app_src/Sources/LemanaVPN/VPNIcons.swift"
log_info "Building LemanaVPN.app"
run swift build -c release --package-path "$app_src"
app_bin="$app_src/.build/release/LemanaVPN"
info_plist="$tmp/Info.plist"
if [ "$DRY_RUN" -eq 0 ] && [ ! -x "$app_bin" ]; then
die "Swift build did not produce $app_bin"
fi
run mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
run install -m 755 "$app_bin" "$APP_DIR/Contents/MacOS/LemanaVPN"
write_file "$info_plist" '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>LemanaVPN</string>
<key>CFBundleIdentifier</key>
<string>ru.dokops.LemanaVPN</string>
<key>CFBundleName</key>
<string>LemanaVPN</string>
<key>CFBundleDisplayName</key>
<string>LemanaVPN</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>'
run install -m 644 "$info_plist" "$APP_DIR/Contents/Info.plist"
log_ok "Menu Bar app installed: $APP_DIR"
}
install_launch_agent() {
if [ "$INSTALL_AUTOSTART" -ne 1 ]; then
log_skip "Autostart disabled; LemanaVPN.app можно запускать вручную."
return 0
fi
if [ "$INSTALL_APP" -ne 1 ]; then
log_skip "Autostart skipped because Menu Bar app is disabled."
return 0
fi
tmp="$1"
plist="$tmp/ru.dokops.LemanaVPN.plist"
log_step "Настраиваю автозапуск Menu Bar app"
log_detail "LaunchAgent запускает LemanaVPN.app при логине, чтобы VPN был доступен из status bar."
run mkdir -p "$HOME/Library/LaunchAgents"
write_file "$plist" "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>Label</key>
<string>ru.dokops.LemanaVPN</string>
<key>ProgramArguments</key>
<array>
<string>$APP_DIR/Contents/MacOS/LemanaVPN</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>"
run install -m 644 "$plist" "$LAUNCH_AGENT"
if [ "$DRY_RUN" -eq 0 ]; then
launchctl unload "$LAUNCH_AGENT" >/dev/null 2>&1 || true
launchctl load "$LAUNCH_AGENT" >/dev/null 2>&1 || true
else
printf '+ launchctl load %s\n' "$LAUNCH_AGENT"
fi
log_ok "LaunchAgent installed: $LAUNCH_AGENT"
}
restart_running_menu_bar_app() {
[ "$INSTALL_APP" -eq 1 ] || return 0
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ restart LemanaVPN.app if running\n'
return 0
fi
if pgrep -x LemanaVPN >/dev/null 2>&1; then
log_step "Перезапускаю уже запущенное LemanaVPN.app"
log_detail "Это нужно, чтобы status bar приложение сразу увидело свежий CLI и новый код."
killall LemanaVPN >/dev/null 2>&1 || true
sleep 1
open "$APP_DIR" >/dev/null 2>&1 || true
log_ok "LemanaVPN.app restarted"
else
log_skip "LemanaVPN.app сейчас не запущен; перезапуск не нужен."
fi
}
install_shell_aliases() {
if [ "$INSTALL_ALIASES" -ne 1 ]; then
log_skip "Shell aliases disabled; команды можно вызывать напрямую из $INSTALL_BIN_DIR."
return 0
fi
zshrc="$HOME/.zshrc"
tmp="$1"
log_step "Обновляю shell aliases"
log_detail "Основные алиасы vpn, vpn-auto, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc."
log_detail "vpn-manual и vpn-manual-full остаются только как совместимые fallback-команды."
[ -f "$zshrc" ] || run touch "$zshrc"
block="$tmp/zshrc-block"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ update %s aliases\n' "$zshrc"
return 0
fi
cat > "$block" <<EOF
# >>> lemana-vpn
vpn() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" "\$@"; }
vpn-auto() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --auto "\$@"; }
vpn-manual() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --manual "\$@"; }
vpn-manual-full() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --manual-full "\$@"; }
vpn-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; }
vpn-fix-dns() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --repair-dns; }
# <<< lemana-vpn
EOF
awk '
/^# >>> lemana-vpn$/ { skip=1; next }
/^# <<< lemana-vpn$/ { skip=0; next }
skip != 1 { print }
' "$zshrc" > "$tmp/zshrc"
{
cat "$tmp/zshrc"
printf '\n'
cat "$block"
} > "$tmp/zshrc.new"
mv "$tmp/zshrc.new" "$zshrc"
log_ok "Shell aliases updated: $zshrc"
}
maybe_login_bitwarden() {
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
log_skip "Bitwarden не выбран; пропускаю Bitwarden login."
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ bw status # check Bitwarden login state\n'
return 0
fi
if ! command -v bw >/dev/null 2>&1; then
log_warn "Bitwarden CLI enabled but bw not found."
return 0
fi
log_step "Проверяю Bitwarden CLI session"
log_detail "На первом запуске vpn CLI сможет понять, нужно ли делать bw login/unlock."
status="$(bw status 2>/dev/null || true)"
if printf '%s\n' "$status" | grep -q '"status":"unauthenticated"'; then
log_warn "Bitwarden CLI is not logged in. Run later: bw login"
elif printf '%s\n' "$status" | grep -q '"status":"locked"'; then
log_info "Bitwarden CLI is logged in but locked. First vpn run will ask for master password."
else
log_ok "Bitwarden CLI is available."
fi
}
auth_profile_label() {
case "$MODE" in
manual-keychain) printf 'ручной вход: логин и пароль в Keychain, одноразовый код вручную' ;;
keychain-auto) printf 'автоматический вход: ключ на этом Mac' ;;
bitwarden-auto) printf 'автоматический вход: Bitwarden' ;;
*) printf '%s' "$MODE" ;;
esac
}
print_final_next_steps() {
log ""
log_ok "Готово."
log_info "Open a new shell or run: exec zsh"
if [ "$IMPORT_YANDEX_KEY" -eq 1 ] || [ "$MODE" != manual-keychain ]; then
log_info "Как подключиться: автоматический вход командой vpn или кнопкой \"Подключить\"."
else
log_info "Как подключиться: Lemana VPN подставит логин и пароль, одноразовый код введите вручную."
fi
log_detail "Способ входа можно изменить в меню приложения."
log_info "Status: vpn --status"
if [ "$INSTALL_APP" -eq 1 ]; then
log_info "App: open '$APP_DIR'"
fi
}
main() {
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT INT TERM
preserve_existing_username
choose_modules
if [ "$IMPORT_YANDEX_KEY" -eq 1 ]; then
[ "$MODE" != "bitwarden-auto" ] || die "Импорт Яндекс Ключа используется с локальным ключом, без Bitwarden."
if [ -z "$USERNAME" ] || ! keychain_has openconnect-lite "$USERNAME"; then
CONFIGURE_LOGIN=1
fi
fi
log_step "Начинаю установку Lemana VPN"
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
if interactive_enabled; then
log_info "Выбран режим: $(auth_profile_label)"
log_detail "Bitwarden и Touch ID будут установлены только если они выбраны явно."
else
log_info "Modules: mode=$MODE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
fi
install_homebrew_packages
install_openconnect_lite
install_cli "$tmp"
install_config "$tmp"
install_dns_cleanup "$tmp"
install_sudoers "$tmp"
install_touchid_helper "$tmp"
install_menu_bar_app "$tmp"
install_launch_agent "$tmp"
restart_running_menu_bar_app
install_shell_aliases "$tmp"
maybe_login_bitwarden
if [ "$CONFIGURE_LOGIN" -eq 1 ]; then
log_step "Записываю логин и пароль в macOS Keychain"
log_detail "Будет запрошен корпоративный LDAP-пароль; настройка одноразового кода выполняется отдельно."
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-login
elif [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
log_step "Записываю credentials в macOS Keychain"
log_detail "Будут запрошены LDAP-пароль и постоянный код настройки 2FA, не текущий 30-секундный код."
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
fi
if [ "$IMPORT_YANDEX_KEY" -eq 1 ]; then
log_step "Импортирую код из Яндекс Ключа"
log_detail "Выберите локальный файл и запись VPN, затем подтвердите её текущим кодом с телефона."
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --import-yandex-key
fi
print_final_next_steps
}
main "$@"