From 7c625e840e4c5625d749facb7952797855a6bd11 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Wed, 20 May 2026 12:10:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=20=D1=80?= =?UTF-8?q?=D1=83=D1=87=D0=BD=D0=BE=D0=B9=20=D0=B8=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D1=8B=20VPN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 ++++-- app/Sources/LemanaVPN/LemanaVPNApp.swift | 8 +- app/Sources/LemanaVPN/VPNManager.swift | 26 +++++- bin/vpn-lemanapro.sh | 111 +++++++++++++++++++---- install.sh | 4 +- tests/smoke.sh | 38 ++++++++ 6 files changed, 185 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 89eb522..df9d065 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл - опциональный Touch ID helper для мастер-пароля Bitwarden; - Swift Menu Bar app `LemanaVPN.app`; - безопасный DNS cleanup через root-owned wrapper; -- алиасы `vpn`, `vpn-debug`, `vpn-fix-dns`. +- алиасы `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns`. ## Быстрая установка @@ -89,7 +89,7 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \ | `/usr/local/sbin/lemana-vpn-dns-cleanup` | Root-owned wrapper для сброса только корпоративных DNS | | `/etc/sudoers.d/lemana-vpn-openconnect` | `NOPASSWD` только для `openconnect` | | `/etc/sudoers.d/lemana-vpn-dns` | `NOPASSWD` только для DNS cleanup wrapper | -| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-debug`, `vpn-fix-dns` | +| `~/.zshrc` | Идемпотентный блок алиасов `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns` | ## Статус модулей @@ -304,14 +304,23 @@ open ~/Applications/LemanaVPN.app ```sh vpn # подключиться +vpn-auto # автоматический режим: скрытый браузер, auto-fill и submit +vpn-manual # ручной режим: видимый браузер, auto-fill без submit +vpn --manual # то же самое без alias vpn --status # статус без нового подключения vpn --status --json # статус в JSON vpn-debug # видимый браузер и debug-логи -vpn --manual-sso --debug # видимый браузер без auto-fill/auto-submit Keycloak +vpn --manual --debug # ручной режим с debug-логами vpn-fix-dns # сбросить корпоративные DNS после аварийного завершения open ~/Applications/LemanaVPN.app # открыть Swift-приложение в menu bar ``` +Режимы подключения: + +- `auto` — режим по умолчанию. Браузер скрытый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain, Keycloak форма заполняется и отправляется автоматически. +- `manual` — браузер видимый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain и подставляются в поля, но кнопки входа не нажимаются. Нажимаешь сам после проверки формы. +- `--manual-sso` оставлен как совместимый alias для `--manual`. + Первый запуск с Bitwarden: 1. CLI проверит `bw`. @@ -372,7 +381,9 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \ | `input/change events` | Оставляет старое прямое `value = ...`, но после него отправляет DOM events | Keycloak не реагирует на прямую запись value без событий | | `legacy auto-fill` | Сохраняет старую рабочую схему `ApplicationWorld`, прямой `value = ...` и простой `click()` | Это ровно тот режим, на котором hidden SSO раньше стабильно проходил Keycloak | | `URL guard` | Проверяет `location.href` через `new RegExp(...)` перед auto-fill | Qt игнорирует `@include`, без guard auto-fill может кликнуть Cisco ACS и сломать SAML | -| `manual SSO disable` | Позволяет отключить auto-fill через `LEMANA_VPN_AUTOFILL_DISABLE=1` | Нужен для ручной диагностики в видимом браузере | +| `auth redirect` | Читает 302 с `vpn.lemanapro.ru` без автоматического follow-redirect | Python `requests` может падать на TLS reset при открытии `/` на `sslvpna/b`, хотя для SAML нужен только конечный host | +| `manual submit gate` | Позволяет отключить только auto-click через `LEMANA_VPN_AUTOFILL_CLICK=0` | Ручной режим видит заполненную форму, но сам решает, когда нажать вход | +| `manual SSO disable` | Позволяет полностью отключить auto-fill через `LEMANA_VPN_AUTOFILL_DISABLE=1` | Нужен для низкоуровневой диагностики без подстановки полей | Перед первым изменением CLI сохраняет оригинальный файл: @@ -415,19 +426,21 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | vpn-lemanapro.sh --patch-only ``` +Если лог падает раньше браузера с `SSLEOFError` / `UNEXPECTED_EOF_WHILE_READING` на `sslvpna.lemanapro.ru` или `sslvpnb.lemanapro.ru`, это ломается этап определения конечного Cisco headend. Актуальный runtime-патч `auth redirect` не открывает `/` на `sslvpna/b`, а только берёт `Location` из 302 ответа `vpn.lemanapro.ru` и продолжает штатный SAML init через POST. + Если SSO ломается после обновления `openconnect-lite`, запусти: ```sh vpn-debug ``` -Если нужно самому посмотреть форму Keycloak и исключить влияние автоматического заполнения: +Если нужно самому посмотреть форму Keycloak, но оставить подстановку LDAP/TOTP: ```sh -vpn --manual-sso --debug +vpn --manual ``` -В этом режиме браузер видимый, а `openconnect-lite` не запускает auto-fill/auto-submit. LDAP-пароль и TOTP seed всё ещё берутся из Keychain, но ввод на странице выполняется вручную. +В этом режиме браузер видимый, `openconnect-lite` заполняет поля из Keychain/Bitwarden, но не нажимает submit. Для полной диагностики без подстановки можно отдельно выставить `LEMANA_VPN_AUTOFILL_DISABLE=1`. Если установка падает на строке `install: /usr/local/sbin/...: No such file or directory`, значит на машине не было `/usr/local/sbin`. Актуальный `install.sh` создаёт эту директорию сам; достаточно повторить установку свежей командой `curl`. @@ -437,7 +450,8 @@ CLI перед подключением патчит `openconnect-lite`: - добавляет `input` и `change` events для Keycloak auto-fill, сохраняя старое прямое присваивание `value = ...`; - оставляет auto-fill в старом `ApplicationWorld` и не добавляет stateful click guards/native setters; - добавляет URL guard, чтобы auto-fill не кликал submit на Cisco ACS; -- добавляет manual SSO disable для видимой ручной диагностики без auto-fill. +- добавляет auth redirect patch, чтобы Python не падал на TLS reset при follow-redirect к `sslvpna/b`; +- добавляет manual submit gate для видимой ручной диагностики с auto-fill, но без auto-submit. ## Удаление diff --git a/app/Sources/LemanaVPN/LemanaVPNApp.swift b/app/Sources/LemanaVPN/LemanaVPNApp.swift index 73b2d9e..fa740f7 100644 --- a/app/Sources/LemanaVPN/LemanaVPNApp.swift +++ b/app/Sources/LemanaVPN/LemanaVPNApp.swift @@ -92,10 +92,14 @@ struct VPNMenuView: View { Label("VPN отключён", systemImage: "circle") .disabled(true) Divider() - Button("Подключить") { - vpnManager.connect() + Button("Подключить автоматически") { + vpnManager.connect(mode: .auto) } .keyboardShortcut("c") + Button("Подключить вручную") { + vpnManager.connect(mode: .manual) + } + .keyboardShortcut("m") } } diff --git a/app/Sources/LemanaVPN/VPNManager.swift b/app/Sources/LemanaVPN/VPNManager.swift index 05c7ea6..9a79d1b 100644 --- a/app/Sources/LemanaVPN/VPNManager.swift +++ b/app/Sources/LemanaVPN/VPNManager.swift @@ -111,6 +111,18 @@ enum VPNState: Equatable { } } +enum VPNLaunchMode: String { + case auto + case manual + + var cliArgument: String { + switch self { + case .auto: return "--auto" + case .manual: return "--manual" + } + } +} + @MainActor class VPNManager: ObservableObject { @Published var state: VPNState = .disconnected @@ -131,6 +143,7 @@ class VPNManager: ObservableObject { private var autoReconnectAttempts: Int = 0 private var reconnectTimer: Timer? private var consecutiveHealthFailures: Int = 0 + private var currentLaunchMode: VPNLaunchMode = .auto private let healthCheckInterval: TimeInterval = 10 private let maxAutoReconnectAttempts: Int = 3 @@ -223,12 +236,13 @@ class VPNManager: ObservableObject { } } - func connect() { + func connect(mode: VPNLaunchMode = .auto) { guard !isRunning else { log("connect() called but process already running") return } - log("-- VPN connect requested --") + currentLaunchMode = mode + log("-- VPN connect requested (\(mode.rawValue)) --") refreshStatus() state = .connecting lastError = nil @@ -240,7 +254,7 @@ class VPNManager: ObservableObject { let proc = Process() proc.executableURL = URL(fileURLWithPath: "/bin/bash") - proc.arguments = ["-l", scriptPath, "--json"] + proc.arguments = ["-l", scriptPath, "--json", mode.cliArgument] proc.environment = processEnvironment() let stdoutPipe = Pipe() @@ -430,6 +444,10 @@ class VPNManager: ObservableObject { state = .disconnected userInitiatedDisconnect = false autoReconnectAttempts = 0 + } else if currentLaunchMode == .manual { + log("Manual connection ended; auto-reconnect is disabled for manual mode") + state = .disconnected + autoReconnectAttempts = 0 } else if [0, 2, 130, 143].contains(exitCode) { scheduleAutoReconnect(reason: "session ended (exit \(exitCode))") } else { @@ -462,7 +480,7 @@ class VPNManager: ObservableObject { self.log("Auto-reconnect cancelled (state changed)") return } - self.connect() + self.connect(mode: self.currentLaunchMode) } } } diff --git a/bin/vpn-lemanapro.sh b/bin/vpn-lemanapro.sh index ed7f535..be7a827 100755 --- a/bin/vpn-lemanapro.sh +++ b/bin/vpn-lemanapro.sh @@ -48,7 +48,7 @@ JSON_MODE=false STATUS_MODE=false CONFIGURE_KEYCHAIN_MODE=false PATCH_ONLY_MODE=false -MANUAL_SSO_MODE=false +CONNECT_MODE="${LEMANA_VPN_MODE:-auto}" for arg in "$@"; do case "$arg" in @@ -57,24 +57,35 @@ for arg in "$@"; do --status) STATUS_MODE=true ;; --configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;; --patch-only) PATCH_ONLY_MODE=true ;; - --manual-sso) MANUAL_SSO_MODE=true ;; + --auto|auto) CONNECT_MODE="auto" ;; + --manual|manual|--manual-sso) CONNECT_MODE="manual" ;; --help|-h) cat <<'HELP' -Usage: vpn-lemanapro.sh [--debug] [--json] [--status] [--configure-keychain] [--patch-only] [--manual-sso] +Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only] --status Show current VPN status without connecting --status --json Show current VPN status as JSON - --debug Run visible browser and passthrough debug logs + --auto Hidden browser, auto-fill and auto-submit (default) + --manual Visible browser, auto-fill fields, do not press submit + --manual-sso Compatibility alias for --manual + --debug Passthrough debug logs; also shows browser in auto mode --json Emit JSON Lines events for UI wrappers --configure-keychain Prompt for LDAP password and TOTP secret, then save them to Keychain --patch-only Apply openconnect-lite runtime patches and exit - --manual-sso Show browser and disable Keycloak auto-fill/auto-submit HELP exit 0 ;; esac done +case "$CONNECT_MODE" in + auto|manual) ;; + *) + printf 'Unknown VPN mode: %s. Use --auto or --manual.\n' "$CONNECT_MODE" >&2 + exit 2 + ;; +esac + _emit() { local json="$1" human="$2" if $JSON_MODE; then @@ -145,6 +156,14 @@ _find_webengine_process() { find "$OC_VENV/lib" -path '*/site-packages/openconnect_lite/browser/webengine_process.py' -print -quit 2>/dev/null } +_find_authenticator() { + if [[ -n "${LEMANA_VPN_AUTHENTICATOR:-}" ]]; then + printf '%s\n' "$LEMANA_VPN_AUTHENTICATOR" + return 0 + fi + find "$OC_VENV/lib" -path '*/site-packages/openconnect_lite/authenticator.py' -print -quit 2>/dev/null +} + _module_bool() { if "$@" >/dev/null 2>&1; then printf true @@ -154,14 +173,19 @@ _module_bool() { } _patches_active() { - local wep + local wep authp wep="$(_find_webengine_process)" - [[ -n "$wep" && -f "$wep" ]] || return 1 + authp="$(_find_authenticator)" + [[ -n "$wep" && -f "$wep" && -n "$authp" && -f "$authp" ]] || return 1 grep -q '"offscreen"' "$wep" \ && grep -q 'new Event("input", {{bubbles: true}})' "$wep" \ && grep -q 'new RegExp' "$wep" \ && grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$wep" \ - && ! grep -Eq '__lemanaVpnClicked|valueSetter|ScriptWorldId.MainWorld' "$wep" + && grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$wep" \ + && ! grep -Eq '__lemanaVpnClicked|valueSetter|ScriptWorldId.MainWorld' "$wep" \ + && grep -q 'self.session.get(self.host.vpn_url, allow_redirects=False)' "$authp" \ + && grep -q 'response.headers.get("Location")' "$authp" \ + && ! grep -q 'requests.get(self.host.vpn_url)' "$authp" } _keychain_has() { @@ -317,20 +341,27 @@ _check_status() { } _patch_oc() { - local wep + local wep authp wep="$(_find_webengine_process)" if [[ -z "$wep" || ! -f "$wep" ]]; then printf 'webengine_process.py not found. Run: pipx install openconnect-lite\n' >&2 return 1 fi + authp="$(_find_authenticator)" + if [[ -z "$authp" || ! -f "$authp" ]]; then + printf 'authenticator.py not found. Run: pipx install openconnect-lite\n' >&2 + return 1 + fi - "$OC_PYTHON" - "$wep" "$PATCH_BACKUP_DIR" <<'PY' + "$OC_PYTHON" - "$wep" "$authp" "$PATCH_BACKUP_DIR" <<'PY' from pathlib import Path import sys path = Path(sys.argv[1]) -backup_dir = Path(sys.argv[2]) +auth_path = Path(sys.argv[2]) +backup_dir = Path(sys.argv[3]) backup_file = backup_dir / "webengine_process.py.before-lemana-vpn" +auth_backup_file = backup_dir / "authenticator.py.before-lemana-vpn" src = path.read_text() before = src messages = [] @@ -473,9 +504,10 @@ canonical_selectors = '''def get_selectors(rules, credentials): possibilities=dir(credentials), ) elif rule.action == "click": - statements.append( - f"""var elem = document.querySelector({selector}); if (elem) {{ elem.dispatchEvent(new Event("focus")); elem.click(); }}""" - ) + if os.environ.get("LEMANA_VPN_AUTOFILL_CLICK", "1") != "0": + statements.append( + f"""var elem = document.querySelector({selector}); if (elem) {{ elem.dispatchEvent(new Event("focus")); elem.click(); }}""" + ) return "\\n".join(statements) ''' if selectors_marker not in src: @@ -485,11 +517,53 @@ if src[selectors_start:] != canonical_selectors: src = src[:selectors_start] + canonical_selectors messages.append("input/change events") +auth_src = auth_path.read_text() +auth_before = auth_src + +if "from urllib.parse import urljoin" not in auth_src: + if "import requests\n" not in auth_src: + fail("auth redirect import") + auth_src = auth_src.replace( + "import requests\n", + "import requests\nfrom urllib.parse import urljoin\n", + 1, + ) + messages.append("auth redirect import") + +detect_marker = " def _detect_authentication_target_url(self):\n" +start_marker = " def _start_authentication(self):\n" +canonical_detect = ''' def _detect_authentication_target_url(self): + # Read the Cisco redirect target without opening / on the final headend. + response = self.session.get(self.host.vpn_url, allow_redirects=False) + if response.is_redirect: + location = response.headers.get("Location") + if not location: + response.raise_for_status() + self.host.address = urljoin(self.host.vpn_url, location) + else: + response.raise_for_status() + self.host.address = response.url + logger.debug("Auth target url", url=self.host.vpn_url) + +''' +if detect_marker not in auth_src or start_marker not in auth_src: + fail("auth redirect") +detect_start = auth_src.index(detect_marker) +detect_end = auth_src.index(start_marker, detect_start) +if auth_src[detect_start:detect_end] != canonical_detect: + auth_src = auth_src[:detect_start] + canonical_detect + auth_src[detect_end:] + messages.append("auth redirect") + if src != before: backup_dir.mkdir(parents=True, exist_ok=True) if not backup_file.exists(): backup_file.write_text(before) path.write_text(src) +if auth_src != auth_before: + backup_dir.mkdir(parents=True, exist_ok=True) + if not auth_backup_file.exists(): + auth_backup_file.write_text(auth_before) + auth_path.write_text(auth_src) for message in messages: print(f"Patch applied: {message}") PY @@ -797,10 +871,12 @@ display_mode="hidden" log_level="" autofill_debug="${LEMANA_VPN_AUTOFILL_DEBUG:-0}" autofill_disable="${LEMANA_VPN_AUTOFILL_DISABLE:-0}" -if $MANUAL_SSO_MODE; then +autofill_click="${LEMANA_VPN_AUTOFILL_CLICK:-1}" +if [[ "$CONNECT_MODE" == "manual" ]]; then display_mode="shown" - autofill_disable="1" - _emit '{"event":"manual_sso","autofill":false}' "Manual SSO mode: browser is visible, Keycloak auto-fill is disabled." + autofill_disable="0" + autofill_click="0" + _emit '{"event":"manual_sso","autofill":true,"submit":false}' "Manual mode: browser is visible, fields are auto-filled, submit is not pressed." fi if $DEBUG; then display_mode="shown" @@ -814,6 +890,7 @@ while true; do QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" \ LEMANA_VPN_AUTOFILL_DEBUG="$autofill_debug" \ LEMANA_VPN_AUTOFILL_DISABLE="$autofill_disable" \ + LEMANA_VPN_AUTOFILL_CLICK="$autofill_click" \ "$OC_BIN" --browser-display-mode "$display_mode" $log_level 2>&1 \ | _filter_output exit_code=${PIPESTATUS[0]} diff --git a/install.sh b/install.sh index cd6eddd..6b61ab2 100755 --- a/install.sh +++ b/install.sh @@ -720,7 +720,7 @@ install_shell_aliases() { zshrc="$HOME/.zshrc" tmp="$1" log_step "Обновляю shell aliases" - log_detail "Алиасы vpn, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc." + log_detail "Алиасы vpn, vpn-auto, vpn-manual, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc." [ -f "$zshrc" ] || run touch "$zshrc" @@ -733,6 +733,8 @@ install_shell_aliases() { cat > "$block" <>> 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-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; } vpn-fix-dns() { sudo "$DNS_CLEANUP"; } # <<< lemana-vpn diff --git a/tests/smoke.sh b/tests/smoke.sh index cc98cb8..1b5e304 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -35,10 +35,19 @@ printf '%s\n' "$status_json" | grep -q '"app":' grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh" grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh" +grep -q -- '--auto' "$ROOT/bin/vpn-lemanapro.sh" +grep -q -- '--manual' "$ROOT/bin/vpn-lemanapro.sh" grep -q -- '--manual-sso' "$ROOT/bin/vpn-lemanapro.sh" grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$ROOT/bin/vpn-lemanapro.sh" +grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$ROOT/bin/vpn-lemanapro.sh" +grep -q 'vpn-auto' "$ROOT/install.sh" +grep -q 'vpn-manual' "$ROOT/install.sh" +grep -q 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift" +grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift" +grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift" fake_webengine="$TMP_DIR/webengine_process.py" +fake_authenticator="$TMP_DIR/authenticator.py" cat > "$fake_webengine" <<'PY' import json import sys @@ -97,7 +106,27 @@ def get_selectors(rules, credentials): return "\n".join(statements) PY +cat > "$fake_authenticator" <<'PY' +import requests +import structlog + +logger = structlog.get_logger() + +class Authenticator: + def _detect_authentication_target_url(self): + # Follow possible redirects in a GET request + # Authentication will occur using a POST request on the final URL + response = requests.get(self.host.vpn_url) + response.raise_for_status() + self.host.address = response.url + logger.debug("Auth target url", url=self.host.vpn_url) + + def _start_authentication(self): + pass +PY + LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \ +LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \ LEMANA_VPN_OC_PYTHON=python3 \ LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/patch-backups" \ bash "$ROOT/bin/vpn-lemanapro.sh" --patch-only >/dev/null @@ -107,6 +136,8 @@ grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$fake_webengine" grep -q 'new RegExp' "$fake_webengine" grep -q 'script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)' "$fake_webengine" grep -q 'new Event("input", {{bubbles: true}})' "$fake_webengine" +grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$fake_webengine" +grep -q 'os.environ.get("LEMANA_VPN_AUTOFILL_CLICK", "1") != "0"' "$fake_webengine" if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then echo "patched auto-fill should keep the original ApplicationWorld behavior" >&2 exit 1 @@ -119,6 +150,13 @@ if grep -q 'valueSetter' "$fake_webengine"; then echo "patched auto-fill should use the original direct value assignment with input/change events" >&2 exit 1 fi +grep -q 'from urllib.parse import urljoin' "$fake_authenticator" +grep -q 'self.session.get(self.host.vpn_url, allow_redirects=False)' "$fake_authenticator" +grep -q 'response.headers.get("Location")' "$fake_authenticator" +if grep -q 'requests.get(self.host.vpn_url)' "$fake_authenticator"; then + echo "auth target detection must not follow redirects with bare requests.get" >&2 + exit 1 +fi status_text="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status)" printf '%s\n' "$status_text" | grep -q 'Modules:'