Improve VPN status reporting and recovery

This commit is contained in:
2026-10-13 06:53:52 +03:00
parent fdb15ae462
commit 7a716b4c9c
188 changed files with 6953 additions and 1911 deletions
+350 -97
View File
@@ -10,24 +10,45 @@ export LEMANA_VPN_BIN_DIR="$HOME/bin"
export LEMANA_VPN_CONFIG_DIR="$HOME/.config/lemana-vpn"
export OPENCONNECT_LITE_CONFIG_DIR="$HOME/.config/openconnect-lite"
mkdir -p "$HOME"
# Every connection below uses fake clients; cleanup and status must stay isolated too.
safe_bin="$TMP_DIR/safe-bin"
mkdir -p "$safe_bin"
printf '#!/bin/sh\ncase "$*" in *" --recover") printf "no_snapshot\\n" ;; *" --begin "*) printf "ready\\n" ;; *" --recover-session "*) printf "cancelled\\n" ;; esac\nexit 0\n' > "$safe_bin/sudo"
printf '#!/bin/sh\nexit 0\n' > "$safe_bin/networksetup"
printf '#!/bin/sh\nexit 1\n' > "$safe_bin/security"
chmod +x "$safe_bin/sudo" "$safe_bin/networksetup" "$safe_bin/security"
export PATH="$safe_bin:$PATH"
export LEMANA_VPN_CONNECT_WAIT_SECONDS=0.1
printf '#!/bin/sh\n# LEMANA_DNS_PROTOCOL=2\n# LEMANA_SESSION_PROTOCOL=1\n# LEMANA_RECONNECT_PROTOCOL=1\nprintf "no_snapshot\\n"\n' > "$TMP_DIR/fake-dns-cleanup"
chmod +x "$TMP_DIR/fake-dns-cleanup"
export LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup"
output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)"
printf '%s\n' "$output" | grep -q 'Detected state:'
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
printf '%s\n' "$output" | grep -q 'Modules: auth_profile=manual credential_source=none bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
printf '%s\n' "$output" | grep -q 'Modules: mode=manual-keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
printf '%s\n' "$output" | grep -q 'sudo install -m 755 -o root -g wheel .* /usr/local/sbin/lemana-vpn-dns-cleanup'
printf '%s\n' "$output" | grep -q 'swift build -c release --package-path'
printf '%s\n' "$output" | grep -q 'launchctl load'
printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running'
printf '%s\n' "$output" | grep -q '+ cp .*/bin/vpn-lemanapro.sh '
printf '%s\n' "$output" | grep -q '+ cp .*/bin/lemana-vpn-import-yandex.py '
if printf '%s\n' "$output" | grep -q 'curl -fsSL .*raw/branch/main'; then
echo "checkout install dry-run should use local files, not remote main" >&2
exit 1
fi
import_plan="$(sh "$ROOT/install.sh" --dry-run --non-interactive --import-yandex-key --without-app --no-shell --no-sudoers)"
printf '%s\n' "$import_plan" | grep -q -- 'vpn-lemanapro.sh --configure-login'
printf '%s\n' "$import_plan" | grep -q -- 'vpn-lemanapro.sh --import-yandex-key'
if sh "$ROOT/install.sh" --dry-run --non-interactive --mode bitwarden-auto --import-yandex-key >/dev/null 2>&1; then
echo 'installer must reject mixing Bitwarden mode with local OTP import' >&2
exit 1
fi
esc="$(printf '\033')"
if printf '%s\n' "$output" | grep -q "$esc"; then
echo "non-tty dry-run output contains ANSI color codes" >&2
@@ -39,24 +60,22 @@ if command -v expect >/dev/null 2>&1; then
ROOT="$ROOT" expect <<'EXPECT'
set timeout 30
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
expect "Как подключаться по умолчанию?"
expect "Какой режим использовать?"
expect "Выбор"
send "\r"
expect "Сохранить логин и пароль"
send "y\r"
expect "Импортировать код из Яндекс Ключа"
send "n\r"
expect eof
EXPECT
)"
printf '%s\n' "$interactive_output" | grep -q 'По умолчанию ничего не сохраняется'
printf '%s\n' "$interactive_output" | grep -q 'Как подключаться по умолчанию?'
printf '%s\n' "$interactive_output" | grep -q '1) Вводить всё вручную'
printf '%s\n' "$interactive_output" | grep -q '2) Запомнить логин и пароль'
printf '%s\n' "$interactive_output" | grep -q '3) Настроить автоматический вход'
printf '%s\n' "$interactive_output" | grep -q 'Сохранить логин и пароль в macOS Keychain для подстановки при входе?'
printf '%s\n' "$interactive_output" | grep -q 'Выбран режим: сохранить логин и пароль, одноразовый код вводить вручную'
printf '%s\n' "$interactive_output" | grep -q 'Есть два режима'
printf '%s\n' "$interactive_output" | grep -q 'Какой режим использовать?'
printf '%s\n' "$interactive_output" | grep -q '1) Ручной вход: запомнить логин и пароль'
printf '%s\n' "$interactive_output" | grep -q '2) Автоматический вход'
printf '%s\n' "$interactive_output" | grep -q 'Выбран режим: ручной вход: логин и пароль в Keychain, одноразовый код вручную'
printf '%s\n' "$interactive_output" | grep -q '+ .*/vpn-lemanapro.sh --configure-login'
if printf '%s\n' "$interactive_output" | grep -Eq 'credential_source|auth_profile|TOTP seed'; then
if printf '%s\n' "$interactive_output" | grep -Eq 'credential_source|auth_profile|saved-totp|TOTP seed'; then
echo "basic interactive wizard should not expose technical credential wording" >&2
exit 1
fi
@@ -65,15 +84,18 @@ fi
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$status_json" | grep -q '"modules":'
printf '%s\n' "$status_json" | grep -q '"app":'
printf '%s\n' "$status_json" | grep -q '"app_menu":{"install_label":"Режим: полностью вручную","configured_mode":"manual","available_modes":\["configured"\],"setup_hint":null}'
printf '%s\n' "$status_json" | grep -q '"mode":{"id":"manual-keychain"'
printf '%s\n' "$status_json" | grep -q '"app_menu":{"install_label":"Режим: ручной вход, логин и пароль не настроены","configured_mode":"manual-keychain","available_modes":\["configured","bitwarden-auto"\],"setup_hint":"Настройте в Terminal: vpn --configure-login"}'
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","auth_profile":"manual","login_ready":false,"keychain_ready":false}'
printf '%s\n' "$keychain_status_json" | grep -q '"mode":{"id":"manual-keychain"'
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","auth_profile":"saved-login","login_ready":false,"keychain_ready":false}'
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"enabled":false'
printf '%s\n' "$keychain_status_json" | grep -q '"touchid":{"enabled":false'
bitwarden_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","auth_profile":"manual","login_ready":false,"keychain_ready":false}'
printf '%s\n' "$bitwarden_status_json" | grep -q '"mode":{"id":"bitwarden-auto"'
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","auth_profile":"auto","login_ready":false,"keychain_ready":false}'
printf '%s\n' "$bitwarden_status_json" | grep -q '"bitwarden":{"enabled":true'
status_password_bin="$TMP_DIR/status-password-bin"
@@ -120,39 +142,39 @@ exit 1
SH
chmod +x "$status_full_bin/security"
saved_login_missing_status="$(
manual_keychain_missing_status="$(
LEMANA_VPN_USERNAME="lemana-status-login-missing" \
LEMANA_VPN_AUTH_PROFILE=saved-login \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_MODE=manual-keychain \
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
)"
printf '%s\n' "$saved_login_missing_status" | grep -q '"app_menu":{"install_label":"Режим: логин и пароль не настроены","configured_mode":"saved-login","available_modes":\["manual-full"\],"setup_hint":"Настройте в Terminal: vpn --configure-login"}'
printf '%s\n' "$manual_keychain_missing_status" | grep -q '"mode":{"id":"manual-keychain","label":"Вручную","ready":false'
printf '%s\n' "$manual_keychain_missing_status" | grep -q '"app_menu":{"install_label":"Режим: ручной вход, логин и пароль не настроены","configured_mode":"manual-keychain","available_modes":\["configured","bitwarden-auto"\],"setup_hint":"Настройте в Terminal: vpn --configure-login"}'
saved_login_ready_status="$(
manual_keychain_ready_status="$(
PATH="$status_password_bin:$PATH" \
LEMANA_VPN_USERNAME="lemana-status-login-ready" \
LEMANA_VPN_AUTH_PROFILE=saved-login \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_MODE=manual-keychain \
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
)"
printf '%s\n' "$saved_login_ready_status" | grep -q '"app_menu":{"install_label":"Режим: логин и пароль сохранены","configured_mode":"saved-login","available_modes":\["configured","manual-full"\],"setup_hint":null}'
printf '%s\n' "$manual_keychain_ready_status" | grep -q '"mode":{"id":"manual-keychain","label":"Вручную","ready":true'
printf '%s\n' "$manual_keychain_ready_status" | grep -q '"app_menu":{"install_label":"Режим: ручной вход, логин и пароль сохранены","configured_mode":"manual-keychain","available_modes":\["configured","bitwarden-auto"\],"setup_hint":null}'
saved_totp_missing_status="$(
LEMANA_VPN_USERNAME="lemana-status-totp-missing" \
legacy_saved_totp_status="$(
LEMANA_VPN_USERNAME="lemana-status-legacy-saved-totp" \
LEMANA_VPN_AUTH_PROFILE=saved-totp \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
)"
printf '%s\n' "$saved_totp_missing_status" | grep -q '"app_menu":{"install_label":"Режим: сохранённые данные не настроены","configured_mode":"saved-totp","available_modes":\["manual-full"\],"setup_hint":"Настройте в Terminal: vpn --configure-keychain"}'
printf '%s\n' "$legacy_saved_totp_status" | grep -q '"mode":{"id":"manual-keychain"'
saved_totp_ready_status="$(
bitwarden_auto_ready_status="$(
PATH="$status_full_bin:$PATH" \
LEMANA_VPN_USERNAME="lemana-status-totp-ready" \
LEMANA_VPN_AUTH_PROFILE=saved-totp \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_USERNAME="lemana-status-bitwarden-ready" \
LEMANA_VPN_MODE=bitwarden-auto \
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
)"
printf '%s\n' "$saved_totp_ready_status" | grep -q '"app_menu":{"install_label":"Режим: сохранённые данные без автоотправки","configured_mode":"saved-totp","available_modes":\["configured","saved-login","manual-full","auto"\],"setup_hint":null}'
printf '%s\n' "$bitwarden_auto_ready_status" | grep -q '"mode":{"id":"bitwarden-auto"'
printf '%s\n' "$bitwarden_auto_ready_status" | grep -q '"app_menu":{"install_label":"Автоматически · Bitwarden","configured_mode":"bitwarden-auto","available_modes":\["configured","manual-keychain"\],"setup_hint":null}'
auto_ready_status="$(
PATH="$status_full_bin:$PATH" \
@@ -161,7 +183,7 @@ auto_ready_status="$(
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
)"
printf '%s\n' "$auto_ready_status" | grep -q '"app_menu":{"install_label":"Режим: автоматический вход","configured_mode":"auto","available_modes":\["configured","saved-totp","saved-login","manual-full"\],"setup_hint":null}'
printf '%s\n' "$auto_ready_status" | grep -q '"mode":{"id":"bitwarden-auto"'
personal_ldap='601''03293'
if grep -R "$personal_ldap" "$ROOT/bin" "$ROOT/install.sh" "$ROOT/uninstall.sh" "$ROOT/README.md" "$ROOT/templates" "$ROOT/app" "$ROOT/.agents" >/dev/null; then
echo "personal LDAP username must not be hardcoded in runtime, installer, app, docs, or agent guidance" >&2
@@ -180,11 +202,20 @@ grep -q 'LEMANA_VPN_AUTOFILL_TOTP' "$ROOT/bin/vpn-lemanapro.sh"
grep -q 'vpn-auto' "$ROOT/install.sh"
grep -q 'vpn-manual' "$ROOT/install.sh"
grep -q 'vpn-manual-full' "$ROOT/install.sh"
grep -q 'ForEach(vpnManager.availableLaunchModes)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'vpnManager.connect()' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
if grep -q 'vpnManager.connect(mode:' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"; then
echo 'Connect must use saved configuration, not a launch override' >&2
exit 1
fi
grep -q 'vpnManager.setAuth(' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'vpnManager.installModeLabel' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'vpnManager.installSetupHint' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'Button("Изменить логин и пароль")' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'menuButton("Логин и пароль…"' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'vpnManager.configureLogin()' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'vpnManager.importYandexKey()' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'menuBarExtraStyle(.window)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
grep -q 'AppStorage("menuBarIndicatorStyle")' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
printf '%s\n' "$output" | grep -q 'app/Sources/LemanaVPN/SessionTiming.swift'
if grep -Fq 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"; then
echo "disconnected app menu should not hardcode the auto button" >&2
exit 1
@@ -195,9 +226,7 @@ if grep -Fq 'Button("Подставить только логин и парол
fi
grep -q 'app_menu' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'available_modes' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'menuTitle' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'Подключить полностью вручную' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'case "auth_profile":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'case "mode", "auth_profile":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'case "manual_sso", "auto_sso":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'case "username_required", "username_saved":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'func configureLogin()' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
@@ -213,6 +242,9 @@ import json
import sys
class Browser:
def __init__(self, profile):
page = QWebEnginePage(profile, self)
def run(self, display_mode, credentials, url_pattern, rules):
argv = sys.argv.copy()
if display_mode == "hidden":
@@ -305,7 +337,7 @@ if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then
exit 1
fi
if grep -q '__lemanaVpnClicked' "$fake_webengine"; then
echo "patched auto-fill should stay stateless like the original working setup" >&2
echo "obsolete click flag must not replace the bounded per-attempt guard" >&2
exit 1
fi
if grep -q 'valueSetter' "$fake_webengine"; then
@@ -341,6 +373,7 @@ printf '%s\n' "$uninstall_output" | grep -q 'Проверяю runtime-патчи
printf '%s\n' "$uninstall_output" | grep -q 'Удаляю sudoers и DNS cleanup wrapper'
printf '%s\n' "$uninstall_output" | grep -q 'killall LemanaVPN # if running'
printf '%s\n' "$uninstall_output" | grep -q 'Удаляю VPN-записи из macOS Keychain'
printf '%s\n' "$uninstall_output" | grep -q 'rm -f .*lemana-vpn-import-yandex.py'
if printf '%s\n' "$uninstall_output" | grep -q "$esc"; then
echo "non-tty uninstall dry-run output contains ANSI color codes" >&2
@@ -388,6 +421,24 @@ if printf '%s\n' "$missing_username_output" | grep -q '"event":"keychain_require
echo "missing username should fail before checking Keychain credentials" >&2
exit 1
fi
grep -q '^_tty_available() {' "$ROOT/bin/vpn-lemanapro.sh"
if ! sed -n '/^_prompt_read()/,/^}/p' "$ROOT/bin/vpn-lemanapro.sh" | grep -q 'if _tty_available; then'; then
echo "runtime prompt reads must use the robust /dev/tty availability probe" >&2
exit 1
fi
preserve_username_home="$TMP_DIR/preserve-username-home"
mkdir -p "$preserve_username_home/.config/lemana-vpn" "$preserve_username_home/.config/openconnect-lite"
printf 'LEMANA_VPN_USERNAME="lemana-preserved-user"\nLEMANA_VPN_MODE="bitwarden-auto"\n' > "$preserve_username_home/.config/lemana-vpn/env"
printf 'username = "lemana-preserved-user"\n' > "$preserve_username_home/.config/openconnect-lite/config.toml"
preserve_username_output="$(
cd "$ROOT" &&
HOME="$preserve_username_home" \
LEMANA_VPN_CONFIG_DIR="$preserve_username_home/.config/lemana-vpn" \
OPENCONNECT_LITE_CONFIG_DIR="$preserve_username_home/.config/openconnect-lite" \
sh install.sh --dry-run --non-interactive --mode bitwarden-auto --without-app --no-shell --no-sudoers
)"
printf '%s\n' "$preserve_username_output" | grep -q 'Preserving existing LDAP username from previous config'
fake_path_bin="$TMP_DIR/fake-path-bin"
mkdir -p "$fake_path_bin"
@@ -416,6 +467,9 @@ cat > "$fake_oc_bin" <<'SH'
#!/bin/sh
{
printf 'args=%s\n' "$*"
for arg in "$@"; do
printf 'arg=%s\n' "$arg"
done
printf 'autofill_disable=%s\n' "${LEMANA_VPN_AUTOFILL_DISABLE:-}"
printf 'autofill_click=%s\n' "${LEMANA_VPN_AUTOFILL_CLICK:-}"
printf 'autofill_totp=%s\n' "${LEMANA_VPN_AUTOFILL_TOTP:-}"
@@ -450,11 +504,11 @@ exit 1
SH
chmod +x "$fake_missing_keychain_bin/security"
default_manual_home="$TMP_DIR/default-manual-home"
default_manual_capture="$TMP_DIR/default-manual-capture"
mkdir -p "$default_manual_home"
mkdir -p "$default_manual_home/.config/openconnect-lite"
cat > "$default_manual_home/.config/openconnect-lite/config.toml" <<'TOML'
manual_keychain_missing_home="$TMP_DIR/manual-keychain-missing-home"
manual_keychain_missing_capture="$TMP_DIR/manual-keychain-missing-capture"
mkdir -p "$manual_keychain_missing_home"
mkdir -p "$manual_keychain_missing_home/.config/openconnect-lite"
cat > "$manual_keychain_missing_home/.config/openconnect-lite/config.toml" <<'TOML'
on_disconnect = "true"
[default_profile]
@@ -465,33 +519,34 @@ name = "Default-K"
[credentials]
username = ""
TOML
default_manual_output="$(
HOME="$default_manual_home" \
set +e
manual_keychain_missing_output="$(
HOME="$manual_keychain_missing_home" \
PATH="$fake_missing_keychain_bin:$PATH" \
LEMANA_VPN_CONFIG_DIR="$default_manual_home/.config/lemana-vpn" \
OPENCONNECT_LITE_CONFIG_DIR="$default_manual_home/.config/openconnect-lite" \
LEMANA_VPN_CONFIG_DIR="$manual_keychain_missing_home/.config/lemana-vpn" \
OPENCONNECT_LITE_CONFIG_DIR="$manual_keychain_missing_home/.config/openconnect-lite" \
LEMANA_VPN_USERNAME="lemana-missing-login-$$" \
LEMANA_VPN_MODE=manual-keychain \
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/default-manual-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/default-manual.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$default_manual_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --json
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-keychain-missing-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-keychain-missing.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$manual_keychain_missing_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
)"
manual_keychain_missing_code=$?
set -e
printf '%s\n' "$default_manual_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
if printf '%s\n' "$default_manual_output" | grep -Eq '"event":"(username_required|keychain_required)"'; then
echo "default manual mode should open the browser without saved username or Keychain credentials" >&2
[ "$manual_keychain_missing_code" -ne 0 ]
printf '%s\n' "$manual_keychain_missing_output" | grep -q '"event":"keychain_required"'
printf '%s\n' "$manual_keychain_missing_output" | grep -q 'vpn --configure-login'
if [ -f "$manual_keychain_missing_capture" ]; then
echo "manual-keychain without saved password should fail before launching openconnect-lite" >&2
exit 1
fi
grep -q -- '--browser-display-mode shown' "$default_manual_capture"
grep -q '^autofill_disable=1$' "$default_manual_capture"
grep -q '^autofill_click=0$' "$default_manual_capture"
grep -q '^autofill_totp=0$' "$default_manual_capture"
grep -q '^credentials_block=0$' "$default_manual_capture"
grep -q '^empty_username=0$' "$default_manual_capture"
fake_login_keychain_bin="$TMP_DIR/fake-login-keychain-bin"
mkdir -p "$fake_login_keychain_bin"
@@ -516,57 +571,255 @@ exit 1
SH
chmod +x "$fake_login_keychain_bin/security"
saved_login_capture="$TMP_DIR/saved-login-capture"
saved_login_output="$(
manual_keychain_capture="$TMP_DIR/manual-keychain-capture"
manual_keychain_output="$(
HOME="$HOME" \
PATH="$fake_login_keychain_bin:$PATH" \
LEMANA_VPN_USERNAME="lemana-saved-login-$$" \
LEMANA_VPN_AUTH_PROFILE=saved-login \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_USERNAME="lemana-manual-keychain-$$" \
LEMANA_VPN_MODE=manual-keychain \
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/saved-login-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/saved-login.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$saved_login_capture" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-keychain-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-keychain.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$manual_keychain_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --json
)"
printf '%s\n' "$saved_login_output" | grep -q '"event":"manual_sso","auth_profile":"saved-login","autofill":true,"totp":false,"submit":false'
if printf '%s\n' "$saved_login_output" | grep -q '"event":"keychain_required"'; then
echo "saved-login should not require a TOTP seed" >&2
printf '%s\n' "$manual_keychain_output" | grep -q '"event":"manual_sso","mode":"manual-keychain","autofill":true,"totp":false,"submit":false'
if printf '%s\n' "$manual_keychain_output" | grep -q '"event":"keychain_required"'; then
echo "manual-keychain should not require a TOTP seed" >&2
exit 1
fi
grep -q -- '--browser-display-mode shown' "$saved_login_capture"
grep -q '^autofill_disable=0$' "$saved_login_capture"
grep -q '^autofill_click=0$' "$saved_login_capture"
grep -q '^autofill_totp=0$' "$saved_login_capture"
grep -q -- '--browser-display-mode shown' "$manual_keychain_capture"
grep -q '^autofill_disable=0$' "$manual_keychain_capture"
grep -q '^autofill_click=0$' "$manual_keychain_capture"
grep -q '^autofill_totp=0$' "$manual_keychain_capture"
saved_totp_capture="$TMP_DIR/saved-totp-capture"
saved_totp_output="$(
# Legacy local OTP migrates to automatic local login; explicit manual modes override it.
for local_case in local manual full missing; do
local_capture="$TMP_DIR/otp-$local_case.capture"
local_path="$fake_path_bin:$PATH"
local_arg="--json"
[ "$local_case" != manual ] || local_arg="--manual"
[ "$local_case" != full ] || local_arg="--manual-full"
[ "$local_case" != missing ] || local_path="$fake_login_keychain_bin:$PATH"
set +e
local_output="$(
PATH="$local_path" \
LEMANA_VPN_USERNAME="lemana-local-otp" \
LEMANA_VPN_MODE=manual-keychain \
LEMANA_VPN_LOCAL_OTP=1 \
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/local-otp-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/otp-$local_case.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$local_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" "$local_arg" --json
)"
local_code=$?
set -e
if [ "$local_case" = missing ]; then
[ "$local_code" -ne 0 ]
[ ! -f "$local_capture" ]
printf '%s\n' "$local_output" | grep -q 'vpn --import-yandex-key'
else
[ "$local_code" -eq 0 ]
if [ "$local_case" = local ]; then
grep -q -- '--browser-display-mode hidden' "$local_capture"
grep -q '^autofill_click=1$' "$local_capture"
grep -q '^autofill_totp=1$' "$local_capture"
printf '%s\n' "$local_output" | grep -q '"event":"auto_sso","mode":"keychain-auto","autofill":true,"totp":true,"submit":true'
else
grep -q -- '--browser-display-mode shown' "$local_capture"
grep -q '^autofill_click=0$' "$local_capture"
grep -q '^autofill_totp=0$' "$local_capture"
if [ "$local_case" = full ]; then
grep -q '^autofill_disable=1$' "$local_capture"
else
grep -q '^autofill_disable=0$' "$local_capture"
fi
fi
fi
done
local_status="$(PATH="$fake_login_keychain_bin:$PATH" LEMANA_VPN_USERNAME=lemana-local-otp LEMANA_VPN_LOCAL_OTP=1 bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$local_status" | python3 -c 'import json,sys; m=json.load(sys.stdin)["modules"]; assert m["local_otp"] and not m["mode"]["ready"]; assert "--import-yandex-key" in m["mode"]["setup_hint"]'
fake_bw_sync_bin="$TMP_DIR/fake-bw-sync-bin"
mkdir -p "$fake_bw_sync_bin"
cat > "$fake_bw_sync_bin/bw" <<'SH'
#!/bin/sh
case "${1:-}:${2:-}" in
unlock:*)
printf 'fake-session\n'
;;
get:password)
printf 'ldap-password-from-bitwarden\n'
;;
get:item)
printf '{"login":{"username":"lemana-bw-user","totp":"otpauth://totp/Lemana:test?secret=abcd2345efgh6723&issuer=Lemana"}}\n'
;;
*)
exit 1
;;
esac
SH
cat > "$fake_bw_sync_bin/security" <<'SH'
#!/bin/sh
if [ "${1:-}" = "find-generic-password" ]; then
account=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-a" ]; then
shift
account="${1:-}"
break
fi
shift
done
case "$account" in
lemana-bw-user|totp/lemana-bw-user) exit 0 ;;
*) exit 1 ;;
esac
fi
exit 0
SH
cat > "$fake_bw_sync_bin/keychain-fingerprint" <<'SH'
#!/bin/sh
if [ "${1:-}" = "get" ]; then
printf 'fake-bitwarden-master-password\n'
exit 0
fi
exit 1
SH
fake_store_python="$fake_bw_sync_bin/fake-oc-python"
cat > "$fake_store_python" <<'SH'
#!/bin/sh
if [ "$#" -eq 1 ] && [ "${1:-}" = "-" ] && [ -n "${LEMANA_VPN_CAPTURE_STORE:-}" ]; then
cat >/dev/null
{
printf 'username=%s\n' "${KC_USERNAME:-}"
printf 'password=%s\n' "${_VPN_PASS:-}"
printf 'totp=%s\n' "${_VPN_TOTP_SECRET:-}"
} > "$LEMANA_VPN_CAPTURE_STORE"
exit 0
fi
exec python3 "$@"
SH
chmod +x "$fake_bw_sync_bin/bw" "$fake_bw_sync_bin/security" "$fake_bw_sync_bin/keychain-fingerprint" "$fake_store_python"
bitwarden_username_home="$TMP_DIR/bitwarden-username-home"
bitwarden_username_capture="$TMP_DIR/bitwarden-username-capture"
bitwarden_store_capture="$TMP_DIR/bitwarden-store-capture"
mkdir -p "$bitwarden_username_home/.config/lemana-vpn" "$bitwarden_username_home/.config/openconnect-lite"
printf 'LEMANA_VPN_MODE="bitwarden-auto"\nLEMANA_VPN_USE_BITWARDEN="1"\nLEMANA_VPN_USE_TOUCHID="1"\n' > "$bitwarden_username_home/.config/lemana-vpn/env"
cat > "$bitwarden_username_home/.config/openconnect-lite/config.toml" <<'TOML'
on_disconnect = "true"
[default_profile]
address = "vpn.lemanapro.ru"
user_group = ""
name = "Default-K"
[credentials]
username = ""
TOML
bitwarden_username_output="$(
HOME="$bitwarden_username_home" \
PATH="$fake_bw_sync_bin:$PATH" \
LEMANA_VPN_CONFIG_DIR="$bitwarden_username_home/.config/lemana-vpn" \
OPENCONNECT_LITE_CONFIG_DIR="$bitwarden_username_home/.config/openconnect-lite" \
LEMANA_VPN_KEYCHAIN_FINGERPRINT="$fake_bw_sync_bin/keychain-fingerprint" \
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
LEMANA_VPN_OC_PYTHON="$fake_store_python" \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/bitwarden-username-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/bitwarden-username.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$bitwarden_username_capture" \
LEMANA_VPN_CAPTURE_STORE="$bitwarden_store_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --json
)"
printf '%s\n' "$bitwarden_username_output" | grep -q '"event":"bw_touchid"'
printf '%s\n' "$bitwarden_username_output" | grep -q '"event":"username_saved","source":"bitwarden"'
printf '%s\n' "$bitwarden_username_output" | grep -q '"event":"auto_sso","mode":"bitwarden-auto","autofill":true,"totp":true,"submit":true'
if printf '%s\n' "$bitwarden_username_output" | grep -q '"event":"username_required"'; then
echo "bitwarden-auto must read LDAP username from Bitwarden instead of prompting for it" >&2
exit 1
fi
grep -q '^username=lemana-bw-user$' "$bitwarden_store_capture"
grep -q '^password=ldap-password-from-bitwarden$' "$bitwarden_store_capture"
grep -q '^totp=ABCD2345EFGH6723$' "$bitwarden_store_capture"
grep -q 'LEMANA_VPN_USERNAME="lemana-bw-user"' "$bitwarden_username_home/.config/lemana-vpn/env"
grep -q 'username = "lemana-bw-user"' "$bitwarden_username_home/.config/openconnect-lite/config.toml"
grep -q -- '--browser-display-mode hidden' "$bitwarden_username_capture"
bitwarden_auto_capture="$TMP_DIR/bitwarden-auto-capture"
bitwarden_auto_output="$(
HOME="$HOME" \
PATH="$fake_path_bin:$PATH" \
LEMANA_VPN_USERNAME="lemana-saved-totp-$$" \
LEMANA_VPN_AUTH_PROFILE=saved-totp \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_USERNAME="lemana-bitwarden-auto-$$" \
LEMANA_VPN_MODE=bitwarden-auto \
LEMANA_VPN_LOCAL_OTP=1 \
LEMANA_VPN_USE_TOUCHID=0 \
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/saved-totp-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/saved-totp.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$saved_totp_capture" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/bitwarden-auto-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/bitwarden-auto.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$bitwarden_auto_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --json
)"
printf '%s\n' "$saved_totp_output" | grep -q '"event":"manual_sso","auth_profile":"saved-totp","autofill":true,"totp":true,"submit":false'
grep -q -- '--browser-display-mode shown' "$saved_totp_capture"
grep -q '^autofill_disable=0$' "$saved_totp_capture"
grep -q '^autofill_click=0$' "$saved_totp_capture"
grep -q '^autofill_totp=1$' "$saved_totp_capture"
printf '%s\n' "$bitwarden_auto_output" | grep -q '"event":"auto_sso","mode":"bitwarden-auto","autofill":true,"totp":true,"submit":true'
grep -q -- '--browser-display-mode hidden' "$bitwarden_auto_capture"
grep -q '^arg=--script$' "$bitwarden_auto_capture"
grep -Fq "arg=$TMP_DIR/fake-dns-cleanup --vpnc" "$bitwarden_auto_capture"
grep -q '^autofill_disable=0$' "$bitwarden_auto_capture"
grep -q '^autofill_click=1$' "$bitwarden_auto_capture"
grep -q '^autofill_totp=1$' "$bitwarden_auto_capture"
# A normal Connect has no mode argument and must use the last saved selection.
profile_bin="$TMP_DIR/profile-bin"
mkdir -p "$profile_bin"
printf '#!/bin/sh\nprintf '\''{"status":"locked"}\\n'\''\n' > "$profile_bin/bw"
chmod +x "$profile_bin/bw"
profile_home="$TMP_DIR/profiles"
mkdir -p "$profile_home/config"
printf 'LEMANA_VPN_USERNAME="fixture-profile"\nLEMANA_VPN_USE_TOUCHID="0"\n' > "$profile_home/config/env"
profile_cli() {
HOME="$profile_home" PATH="$profile_bin:$fake_path_bin:$PATH" \
LEMANA_VPN_CONFIG_DIR="$profile_home/config" \
LEMANA_VPN_OC_BIN="$fake_oc_bin" LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$profile_home/patches" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$profile_home/connect.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$profile_home/launch" \
bash "$ROOT/bin/vpn-lemanapro.sh" "$@"
}
for profile in manual keychain bitwarden; do
profile_cli "--set-auth=$profile" >/dev/null
profile_cli --json > "$profile_home/events"
if [ "$profile" = manual ]; then
grep -q -- '--browser-display-mode shown' "$profile_home/launch"
grep -q '^autofill_totp=0$' "$profile_home/launch"
grep -q '^autofill_click=0$' "$profile_home/launch"
else
grep -q -- '--browser-display-mode hidden' "$profile_home/launch"
grep -q '^autofill_totp=1$' "$profile_home/launch"
grep -q '^autofill_click=1$' "$profile_home/launch"
grep -q '"event":"auto_sso"' "$profile_home/events"
fi
done
manual_full_no_credentials_home="$TMP_DIR/manual-full-no-credentials-home"
manual_full_no_credentials_capture="$TMP_DIR/manual-full-no-credentials-capture"
@@ -594,13 +847,13 @@ manual_full_no_credentials_output="$(
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-full-no-credentials-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-full-no-credentials.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$manual_full_no_credentials_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
)"
printf '%s\n' "$manual_full_no_credentials_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
printf '%s\n' "$manual_full_no_credentials_output" | grep -q '"event":"manual_sso","mode":"manual-keychain","diagnostic":true,"autofill":false,"totp":false,"submit":false'
if printf '%s\n' "$manual_full_no_credentials_output" | grep -Eq '"event":"(username_required|keychain_required)"'; then
echo "manual-full should not require saved username or Keychain credentials before opening the browser" >&2
exit 1
@@ -640,7 +893,7 @@ spawn env \
LEMANA_VPN_WEBENGINE_PROCESS=$env(FAKE_WEBENGINE) \
LEMANA_VPN_AUTHENTICATOR=$env(FAKE_AUTHENTICATOR) \
LEMANA_VPN_PATCH_BACKUP_DIR=$env(TMP_DIR)/first-username-patch-backups \
LEMANA_VPN_DNS_CLEANUP=$env(TMP_DIR)/no-dns-cleanup \
LEMANA_VPN_DNS_CLEANUP=$env(TMP_DIR)/fake-dns-cleanup \
LEMANA_VPN_CONNECT_LOG=$env(TMP_DIR)/first-username.log \
LEMANA_VPN_CAPTURE_LAUNCH=$env(MANUAL_FULL_CAPTURE) \
bash $env(ROOT)/bin/vpn-lemanapro.sh --manual --json
@@ -663,13 +916,13 @@ manual_full_output="$(
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-full-patch-backups" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/fake-dns-cleanup" \
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-full.log" \
LEMANA_VPN_CAPTURE_LAUNCH="$manual_full_capture" \
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
)"
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","mode":"manual-keychain","diagnostic":true,"autofill":false,"totp":false,"submit":false'
grep -q -- '--browser-display-mode shown' "$manual_full_capture"
grep -q '^autofill_disable=1$' "$manual_full_capture"
grep -q '^autofill_click=0$' "$manual_full_capture"