Files
lemana-vpn/install.sh

944 lines
29 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
USE_BITWARDEN=1
USE_TOUCHID=1
INSTALL_SUDOERS=1
INSTALL_ALIASES=1
INSTALL_APP=1
INSTALL_AUTOSTART=1
CONFIGURE_KEYCHAIN=0
DRY_RUN=0
FORCE=0
INTERACTIVE=auto
BITWARDEN_FORCED=0
CREDENTIAL_SOURCE_FORCED=0
TOUCHID_FORCED=0
SUDOERS_FORCED=0
SHELL_FORCED=0
APP_FORCED=0
AUTOSTART_FORCED=0
CONFIGURE_KEYCHAIN_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 (default)
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
--credential-source VALUE Credential source: bitwarden or keychain
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
--without-touchid Do not install/use Touch ID helper
--configure-keychain Prompt for LDAP password and TOTP secret after install
--username VALUE Corporate LDAP username (default: 60103293)
--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 Same as --without-bitwarden --without-touchid
--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 --minimal --configure-keychain
sh install.sh --credential-source keychain --configure-keychain
sh install.sh --without-touchid
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--with-bitwarden)
CREDENTIAL_SOURCE="bitwarden"
USE_BITWARDEN=1
BITWARDEN_FORCED=1
CREDENTIAL_SOURCE_FORCED=1
;;
--without-bitwarden)
CREDENTIAL_SOURCE="keychain"
USE_BITWARDEN=0
USE_TOUCHID=0
BITWARDEN_FORCED=1
CREDENTIAL_SOURCE_FORCED=1
;;
--credential-source)
shift
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
CREDENTIAL_SOURCE="$1"
CREDENTIAL_SOURCE_FORCED=1
case "$CREDENTIAL_SOURCE" in
bitwarden)
USE_BITWARDEN=1
;;
keychain)
USE_BITWARDEN=0
USE_TOUCHID=0
;;
*)
echo "--credential-source requires bitwarden or keychain" >&2
exit 1
;;
esac
;;
--with-touchid)
USE_TOUCHID=1
TOUCHID_FORCED=1
;;
--without-touchid)
USE_TOUCHID=0
TOUCHID_FORCED=1
;;
--configure-keychain)
CONFIGURE_KEYCHAIN=1
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)
CREDENTIAL_SOURCE="keychain"
USE_BITWARDEN=0
USE_TOUCHID=0
BITWARDEN_FORCED=1
CREDENTIAL_SOURCE_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
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"
}
has_tty() {
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } || [ -t 0 ]
}
interactive_enabled() {
case "$INTERACTIVE" in
1) has_tty ;;
0) return 1 ;;
auto) has_tty ;;
*) return 1 ;;
esac
}
prompt_printf() {
if [ -w /dev/tty ]; then
printf "$@" > /dev/tty
else
printf "$@"
fi
}
prompt_read_answer() {
if [ -r /dev/tty ]; then
IFS= read -r answer < /dev/tty || answer=""
else
IFS= read -r answer || answer=""
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
}
choose_credential_source_interactive() {
log_step "Настраиваю способ хранения credentials"
log_detail "Можно выбрать Bitwarden или бесплатный macOS Keychain flow с ручным вводом LDAP password и TOTP seed."
while :; do
prompt_printf '\n'
prompt_printf '%sКак хранить VPN credentials?%s\n' "$C_BOLD" "$C_RESET"
prompt_printf ' 1) Bitwarden -> macOS Keychain\n'
prompt_printf ' 2) macOS Keychain: ввести LDAP password и TOTP seed сейчас\n'
prompt_printf ' 3) macOS Keychain: настрою вручную позже\n'
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
prompt_read_answer
case "$answer" in
""|1)
CREDENTIAL_SOURCE=bitwarden
USE_BITWARDEN=1
;;
2)
CREDENTIAL_SOURCE=keychain
USE_BITWARDEN=0
USE_TOUCHID=0
CONFIGURE_KEYCHAIN=1
;;
3)
CREDENTIAL_SOURCE=keychain
USE_BITWARDEN=0
USE_TOUCHID=0
CONFIGURE_KEYCHAIN=0
;;
*)
prompt_printf 'Введите 1, 2 или 3.\n'
continue
;;
esac
break
done
case "$CREDENTIAL_SOURCE" in
bitwarden)
log_info "Credential source: Bitwarden sync into macOS Keychain"
;;
keychain)
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
log_info "Credential source: macOS Keychain, credentials будут запрошены после установки"
else
log_info "Credential source: macOS Keychain, credentials можно настроить позже через vpn --configure-keychain"
fi
;;
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 TOTP seed: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
}
choose_modules() {
print_detected_state
case "$CREDENTIAL_SOURCE" in
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 "Установщик проведёт по выбору credential source и отсутствующих опциональных модулей; флаги командной строки имеют приоритет."
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
choose_credential_source_interactive
fi
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
CREDENTIAL_SOURCE=bitwarden
USE_BITWARDEN=1
else
CREDENTIAL_SOURCE=keychain
USE_BITWARDEN=0
USE_TOUCHID=0
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 helper не найден. Собрать и установить?" 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_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then
CONFIGURE_KEYCHAIN=1
fi
fi
fi
}
script_dir() {
case "$0" in
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
*) return 1 ;;
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"
}
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-пароль и TOTP seed из 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 "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"
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
sed "s/{{USERNAME}}/$USERNAME/g" "$tmp/openconnect-lite-config.toml" > "$OC_CONFIG_DIR/config.toml"
chmod 600 "$OC_CONFIG_DIR/config.toml"
fi
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\""
write_file "$tmp/env" "$env_content"
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 сбрасывает только похожие на корпоративные 10.x 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
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"
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-manual, vpn-manual-full, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc."
[ -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() { sudo "$DNS_CLEANUP"; }
# <<< 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 "Credential source is keychain; пропускаю Bitwarden login."
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
}
main() {
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT INT TERM
choose_modules
log_step "Начинаю установку Lemana VPN"
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
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_KEYCHAIN" -eq 1 ]; then
log_step "Записываю credentials в macOS Keychain"
log_detail "Будут запрошены LDAP-пароль и постоянный TOTP seed, не текущий 30-секундный код."
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
fi
log ""
log_ok "Done."
log_info "Open a new shell or run: exec zsh"
log_info "Connect: vpn"
log_info "Status: vpn --status"
if [ "$INSTALL_APP" -eq 1 ]; then
log_info "App: open '$APP_DIR'"
fi
}
main "$@"