Improve VPN status reporting and recovery
This commit is contained in:
+336
-185
@@ -9,25 +9,82 @@ 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}"
|
||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-none}"
|
||||
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=0
|
||||
USE_TOUCHID=0
|
||||
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
|
||||
@@ -81,12 +138,14 @@ Usage:
|
||||
Options:
|
||||
--with-bitwarden Install/use Bitwarden CLI module as an optional credential source
|
||||
--without-bitwarden Do not install/use Bitwarden CLI (default)
|
||||
--credential-source VALUE Credential source: none, keychain, or bitwarden
|
||||
--auth-profile VALUE Auth profile: manual, saved-login, saved-totp, or auto
|
||||
--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
|
||||
@@ -105,50 +164,107 @@ Options:
|
||||
|
||||
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 --credential-source keychain --auth-profile saved-totp --configure-keychain
|
||||
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)
|
||||
CREDENTIAL_SOURCE="bitwarden"
|
||||
USE_BITWARDEN=1
|
||||
MODE="bitwarden-auto"
|
||||
MODE_FORCED=1
|
||||
apply_mode_defaults
|
||||
BITWARDEN_FORCED=1
|
||||
CREDENTIAL_SOURCE_FORCED=1
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "manual" ]; then
|
||||
AUTH_PROFILE="saved-totp"
|
||||
fi
|
||||
;;
|
||||
--without-bitwarden)
|
||||
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ]; then
|
||||
CREDENTIAL_SOURCE="none"
|
||||
fi
|
||||
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)
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
;;
|
||||
bitwarden)
|
||||
USE_BITWARDEN=1
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "manual" ]; then
|
||||
AUTH_PROFILE="saved-totp"
|
||||
none|keychain)
|
||||
if [ "$MODE_FORCED" -eq 0 ]; then
|
||||
MODE="manual-keychain"
|
||||
MANUAL_FORCED=1
|
||||
fi
|
||||
;;
|
||||
keychain)
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
bitwarden)
|
||||
if [ "$MODE_FORCED" -eq 0 ]; then
|
||||
MODE="bitwarden-auto"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "--credential-source requires none, keychain, or bitwarden" >&2
|
||||
@@ -162,7 +278,17 @@ while [ "$#" -gt 0 ]; do
|
||||
AUTH_PROFILE="$1"
|
||||
AUTH_PROFILE_FORCED=1
|
||||
case "$AUTH_PROFILE" in
|
||||
manual|saved-login|saved-totp|auto) ;;
|
||||
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
|
||||
@@ -178,30 +304,28 @@ while [ "$#" -gt 0 ]; do
|
||||
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
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ]; then
|
||||
AUTH_PROFILE="saved-login"
|
||||
fi
|
||||
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||
CREDENTIAL_SOURCE="keychain"
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=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
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ]; then
|
||||
AUTH_PROFILE="saved-totp"
|
||||
fi
|
||||
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||
CREDENTIAL_SOURCE="keychain"
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
fi
|
||||
;;
|
||||
--username)
|
||||
shift
|
||||
@@ -247,10 +371,10 @@ while [ "$#" -gt 0 ]; do
|
||||
--interactive) INTERACTIVE=1 ;;
|
||||
--non-interactive) INTERACTIVE=0 ;;
|
||||
--minimal)
|
||||
CREDENTIAL_SOURCE="none"
|
||||
AUTH_PROFILE="manual"
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
MANUAL_FORCED=1
|
||||
MODE="manual-keychain"
|
||||
MODE_FORCED=1
|
||||
apply_mode_defaults
|
||||
BITWARDEN_FORCED=1
|
||||
CREDENTIAL_SOURCE_FORCED=1
|
||||
AUTH_PROFILE_FORCED=1
|
||||
@@ -271,6 +395,9 @@ while [ "$#" -gt 0 ]; do
|
||||
shift
|
||||
done
|
||||
|
||||
[ -z "$MODE" ] || MODE_FORCED=1
|
||||
normalize_mode_from_legacy
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
@@ -326,8 +453,13 @@ 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() {
|
||||
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } || [ -t 0 ]
|
||||
tty_available || [ -t 0 ]
|
||||
}
|
||||
|
||||
interactive_enabled() {
|
||||
@@ -340,7 +472,7 @@ interactive_enabled() {
|
||||
}
|
||||
|
||||
prompt_printf() {
|
||||
if [ -w /dev/tty ]; then
|
||||
if tty_available; then
|
||||
printf "$@" > /dev/tty
|
||||
else
|
||||
printf "$@"
|
||||
@@ -348,13 +480,19 @@ prompt_printf() {
|
||||
}
|
||||
|
||||
prompt_read_answer() {
|
||||
if [ -r /dev/tty ]; then
|
||||
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"
|
||||
@@ -385,119 +523,60 @@ yes_no() {
|
||||
|
||||
print_install_intro() {
|
||||
log_step "Lemana VPN установит приложение для подключения к корпоративному VPN"
|
||||
log_detail "По умолчанию ничего не сохраняется: при подключении откроется окно входа, а логин, пароль и одноразовый код вводятся вручную."
|
||||
log_detail "Дополнительные удобства можно включить сейчас или позже."
|
||||
}
|
||||
|
||||
choose_advanced_signin_interactive() {
|
||||
while :; do
|
||||
prompt_printf '\n'
|
||||
prompt_printf '%sАвтоматический вход требует сохранённых данных.%s\n' "$C_BOLD" "$C_RESET"
|
||||
prompt_printf ' Автоматический вход работает только если на Mac сохранены корпоративный пароль\n'
|
||||
prompt_printf ' и постоянный код настройки 2FA. Это не текущие 6 цифр из приложения.\n\n'
|
||||
prompt_printf ' 1) Вернуться к ручному режиму\n'
|
||||
prompt_printf ' 2) Сохранить пароль и постоянный код 2FA в macOS Keychain\n'
|
||||
prompt_printf ' 3) Взять пароль и код 2FA из Bitwarden\n'
|
||||
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
||||
|
||||
prompt_read_answer
|
||||
case "$answer" in
|
||||
""|1)
|
||||
AUTH_PROFILE=manual
|
||||
CREDENTIAL_SOURCE=none
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
;;
|
||||
2)
|
||||
AUTH_PROFILE=saved-totp
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=1
|
||||
;;
|
||||
3)
|
||||
AUTH_PROFILE=saved-totp
|
||||
CREDENTIAL_SOURCE=bitwarden
|
||||
USE_BITWARDEN=1
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
;;
|
||||
*)
|
||||
prompt_printf 'Введите 1, 2 или 3. Если не уверены, нажмите Enter.\n'
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
break
|
||||
done
|
||||
log_detail "Есть два режима: ручной ввод кода и автоматический вход. Для автоматического входа выберите ключ на Mac или Bitwarden."
|
||||
log_detail "Ключ из Яндекс Ключа можно импортировать отдельно, без Bitwarden."
|
||||
}
|
||||
|
||||
choose_auth_profile_interactive() {
|
||||
log_step "Выбираю режим подключения"
|
||||
log_detail "Если не уверены, просто нажмите Enter: будет ручной режим без сохранения паролей."
|
||||
log_detail "Если не уверены, просто нажмите Enter: будет ручной режим с сохранением логина и пароля."
|
||||
|
||||
while :; do
|
||||
prompt_printf '\n'
|
||||
prompt_printf '%sКак подключаться по умолчанию?%s\n' "$C_BOLD" "$C_RESET"
|
||||
prompt_printf ' 1) Вводить всё вручную (рекомендуется)\n'
|
||||
prompt_printf ' Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.\n\n'
|
||||
prompt_printf ' 2) Запомнить логин и пароль на этом Mac\n'
|
||||
prompt_printf ' При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.\n\n'
|
||||
prompt_printf ' 3) Настроить автоматический вход\n'
|
||||
prompt_printf ' Нужно заранее сохранить пароль и постоянный код настройки 2FA.\n'
|
||||
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
||||
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)
|
||||
AUTH_PROFILE=manual
|
||||
CREDENTIAL_SOURCE=none
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
if yes_no "Сохранить логин и пароль в macOS Keychain для подстановки при входе?" n; then
|
||||
AUTH_PROFILE=saved-login
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
CONFIGURE_LOGIN=1
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
AUTH_PROFILE=saved-login
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
MANUAL_FORCED=1
|
||||
MODE=manual-keychain
|
||||
apply_mode_defaults
|
||||
CONFIGURE_LOGIN=1
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
;;
|
||||
3)
|
||||
choose_advanced_signin_interactive
|
||||
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 или 3. Если не уверены, нажмите Enter.\n'
|
||||
prompt_printf 'Введите 1 или 2. Если не уверены, нажмите Enter.\n'
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
break
|
||||
done
|
||||
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
log_info "Режим подключения: ручной ввод логина, пароля и одноразового кода"
|
||||
case "$MODE" in
|
||||
manual-keychain)
|
||||
log_info "Режим подключения: логин и пароль в macOS Keychain, одноразовый код вручную"
|
||||
;;
|
||||
saved-login)
|
||||
log_info "Режим подключения: логин и пароль будут сохранены в macOS Keychain, одноразовый код вводится вручную"
|
||||
keychain-auto)
|
||||
log_info "Режим подключения: автоматический, ключ на этом Mac"
|
||||
;;
|
||||
saved-totp)
|
||||
log_info "Режим подключения: сохранённые данные будут подставляться в форму входа"
|
||||
;;
|
||||
auto)
|
||||
log_info "Режим подключения: автоматический вход включается только через явный auto-запуск"
|
||||
bitwarden-auto)
|
||||
log_info "Режим подключения: автоматический, Bitwarden"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -579,26 +658,25 @@ choose_modules() {
|
||||
log_info "Interactive prompts: on"
|
||||
log_detail "Установщик проведёт по понятному выбору режима входа; флаги командной строки имеют приоритет."
|
||||
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||
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
|
||||
CREDENTIAL_SOURCE=bitwarden
|
||||
USE_BITWARDEN=1
|
||||
MODE=bitwarden-auto
|
||||
apply_mode_defaults
|
||||
else
|
||||
AUTH_PROFILE=manual
|
||||
CREDENTIAL_SOURCE=none
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
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?" n; then
|
||||
if yes_no "Разрешить Touch ID для открытия Bitwarden?" y; then
|
||||
USE_TOUCHID=1
|
||||
else
|
||||
USE_TOUCHID=0
|
||||
@@ -644,7 +722,7 @@ choose_modules() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CONFIGURE_LOGIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-login" ] && [ "$CONFIGURE_LOGIN" -eq 0 ]; then
|
||||
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
|
||||
@@ -652,13 +730,20 @@ choose_modules() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-totp" ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
||||
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() {
|
||||
@@ -703,6 +788,37 @@ write_file() {
|
||||
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
|
||||
|
||||
@@ -761,6 +877,9 @@ install_cli() {
|
||||
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"
|
||||
@@ -768,6 +887,7 @@ install_cli() {
|
||||
|
||||
install_config() {
|
||||
tmp="$1"
|
||||
mode_template="$tmp/$MODE.env"
|
||||
log_step "Записываю конфигурацию"
|
||||
log_detail "Здесь сохраняются выбранный режим входа, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
|
||||
|
||||
@@ -777,18 +897,35 @@ install_config() {
|
||||
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"
|
||||
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
|
||||
|
||||
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
||||
LEMANA_VPN_AUTH_PROFILE=\"$AUTH_PROFILE\"
|
||||
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"
|
||||
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"
|
||||
}
|
||||
@@ -798,7 +935,7 @@ install_dns_cleanup() {
|
||||
dns_cleanup_dir="$(dirname "$DNS_CLEANUP")"
|
||||
|
||||
log_step "Устанавливаю DNS cleanup wrapper"
|
||||
log_detail "Wrapper сбрасывает только похожие на корпоративные 10.x DNS после аварийного завершения VPN."
|
||||
log_detail "Wrapper сохраняет прежние DNS и восстанавливает изменения VPN после отключения или перезагрузки."
|
||||
log_detail "macOS может запросить sudo-пароль для записи в $dns_cleanup_dir."
|
||||
|
||||
download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup"
|
||||
@@ -814,6 +951,11 @@ install_sudoers() {
|
||||
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."
|
||||
|
||||
@@ -881,6 +1023,9 @@ install_menu_bar_app() {
|
||||
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"
|
||||
@@ -994,7 +1139,8 @@ install_shell_aliases() {
|
||||
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."
|
||||
log_detail "Основные алиасы vpn, vpn-auto, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc."
|
||||
log_detail "vpn-manual и vpn-manual-full остаются только как совместимые fallback-команды."
|
||||
|
||||
[ -f "$zshrc" ] || run touch "$zshrc"
|
||||
|
||||
@@ -1011,7 +1157,7 @@ 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"; }
|
||||
vpn-fix-dns() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --repair-dns; }
|
||||
# <<< lemana-vpn
|
||||
EOF
|
||||
|
||||
@@ -1036,6 +1182,10 @@ maybe_login_bitwarden() {
|
||||
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
|
||||
@@ -1055,12 +1205,11 @@ maybe_login_bitwarden() {
|
||||
}
|
||||
|
||||
auth_profile_label() {
|
||||
case "$AUTH_PROFILE" in
|
||||
manual) printf 'ручной ввод без сохранения паролей' ;;
|
||||
saved-login) printf 'сохранить логин и пароль, одноразовый код вводить вручную' ;;
|
||||
saved-totp) printf 'подставлять сохранённые данные в окно входа' ;;
|
||||
auto) printf 'автоматический вход только при явном запуске' ;;
|
||||
*) printf '%s' "$AUTH_PROFILE" ;;
|
||||
case "$MODE" in
|
||||
manual-keychain) printf 'ручной вход: логин и пароль в Keychain, одноразовый код вручную' ;;
|
||||
keychain-auto) printf 'автоматический вход: ключ на этом Mac' ;;
|
||||
bitwarden-auto) printf 'автоматический вход: Bitwarden' ;;
|
||||
*) printf '%s' "$MODE" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -1068,23 +1217,12 @@ print_final_next_steps() {
|
||||
log ""
|
||||
log_ok "Готово."
|
||||
log_info "Open a new shell or run: exec zsh"
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
log_info "Как подключиться: откройте LemanaVPN.app и нажмите \"Подключить\", или выполните: vpn"
|
||||
log_detail "Откроется окно входа. Введите корпоративный логин, пароль и одноразовый код вручную."
|
||||
;;
|
||||
saved-login)
|
||||
log_info "Как подключиться: Lemana VPN подставит логин и пароль, одноразовый код введите вручную."
|
||||
log_detail "Команда подключения: vpn"
|
||||
;;
|
||||
saved-totp)
|
||||
log_info "Как подключиться: Lemana VPN подставит сохранённые данные в видимое окно входа."
|
||||
log_detail "Для полностью автоматического запуска используйте явно: vpn-auto"
|
||||
;;
|
||||
auto)
|
||||
log_info "Автоматический вход включён явно. Если он не сработает, используйте ручной режим: vpn --manual-full"
|
||||
;;
|
||||
esac
|
||||
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'"
|
||||
@@ -1097,7 +1235,14 @@ main() {
|
||||
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 не перезаписываются."
|
||||
@@ -1105,7 +1250,7 @@ main() {
|
||||
log_info "Выбран режим: $(auth_profile_label)"
|
||||
log_detail "Bitwarden и Touch ID будут установлены только если они выбраны явно."
|
||||
else
|
||||
log_info "Modules: auth_profile=$AUTH_PROFILE credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||
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
|
||||
@@ -1123,7 +1268,7 @@ main() {
|
||||
|
||||
if [ "$CONFIGURE_LOGIN" -eq 1 ]; then
|
||||
log_step "Записываю логин и пароль в macOS Keychain"
|
||||
log_detail "Будет запрошен корпоративный LDAP-пароль. Одноразовый код при подключении останется ручным."
|
||||
log_detail "Будет запрошен корпоративный LDAP-пароль; настройка одноразового кода выполняется отдельно."
|
||||
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-login
|
||||
elif [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||
log_step "Записываю credentials в macOS Keychain"
|
||||
@@ -1131,6 +1276,12 @@ main() {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user