From 94863e50bd97faafc11beeb684853ab0e7899920 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Wed, 20 May 2026 09:57:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D1=8C=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=BA=D0=BB=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20Keycloak=20submit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++++++++- bin/vpn-lemanapro.sh | 45 +++++++++++++++++++++++++++++++++++++++++--- tests/smoke.sh | 2 ++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b8c7ad9..b59b805 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \ ## Runtime-патчи openconnect-lite -`openconnect-lite` работает, но для текущей macOS + Keycloak SSO цепочки ему нужны три runtime-патча. CLI применяет их перед подключением в файле: +`openconnect-lite` работает, но для текущей macOS + Keycloak SSO цепочки ему нужны runtime-патчи. CLI применяет их перед подключением в файле: ```sh ~/.local/pipx/venvs/openconnect-lite/lib/python*/site-packages/openconnect_lite/browser/webengine_process.py @@ -370,6 +370,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \ | `minimal -> offscreen` | Меняет Qt platform mode для скрытого браузера | `minimal` падает с Qt WebEngine на macOS | | `input/change events` | После `value = ...` отправляет DOM events | Keycloak не реагирует на прямую запись value | | `URL guard` | Проверяет `location.href` через `new RegExp(...)` перед auto-fill | Qt игнорирует `@include`, без guard auto-fill может кликнуть Cisco ACS и сломать SAML | +| `submit click guard` | Кликает submit один раз на страницу и только после заполнения поля | Без guard hidden-браузер может зациклиться на Keycloak `login-actions/authenticate` | Перед первым изменением CLI сохраняет оригинальный файл: @@ -405,6 +406,13 @@ tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log В обычном режиме CLI также печатает heartbeat `Still waiting for SSO/openconnect-lite...`, чтобы было понятно, что процесс живой. В `vpn-debug` дополнительно показываются raw-логи и видимый браузер. +Если в логе повторяется один и тот же URL вида `employee.auth.lemanapro.ru/realms/employee/login-actions/authenticate`, значит hidden-браузер застрял на Keycloak до перехода в Cisco ACS. Сначала обнови и примени runtime-патчи без подключения: + +```sh +curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh +vpn-lemanapro.sh --patch-only +``` + Если SSO ломается после обновления `openconnect-lite`, запусти: ```sh @@ -418,6 +426,7 @@ CLI перед подключением патчит `openconnect-lite`: - `minimal` -> `offscreen`, чтобы Qt WebEngine не падал на macOS; - добавляет `input` и `change` events для Keycloak auto-fill; - добавляет URL guard, чтобы auto-fill не кликал submit на Cisco ACS. +- добавляет submit click guard, чтобы auto-fill не отправлял одну и ту же Keycloak форму бесконечно. ## Удаление diff --git a/bin/vpn-lemanapro.sh b/bin/vpn-lemanapro.sh index 5262472..9b08c5a 100755 --- a/bin/vpn-lemanapro.sh +++ b/bin/vpn-lemanapro.sh @@ -35,6 +35,7 @@ DEBUG=false JSON_MODE=false STATUS_MODE=false CONFIGURE_KEYCHAIN_MODE=false +PATCH_ONLY_MODE=false for arg in "$@"; do case "$arg" in @@ -42,15 +43,17 @@ for arg in "$@"; do --json) JSON_MODE=true ;; --status) STATUS_MODE=true ;; --configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;; + --patch-only) PATCH_ONLY_MODE=true ;; --help|-h) cat <<'HELP' -Usage: vpn-lemanapro.sh [--debug] [--json] [--status] [--configure-keychain] +Usage: vpn-lemanapro.sh [--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 --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 HELP exit 0 ;; @@ -141,7 +144,8 @@ _patches_active() { [[ -n "$wep" && -f "$wep" ]] || return 1 grep -q '"offscreen"' "$wep" \ && grep -q 'new Event("input", {{bubbles: true}})' "$wep" \ - && grep -q 'new RegExp' "$wep" + && grep -q 'new RegExp' "$wep" \ + && grep -q '__lemanaVpnClicked' "$wep" } _keychain_has() { @@ -322,10 +326,20 @@ if src != original: original = src old_fill = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("blur"));' -new_fill = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("input", {{bubbles: true}})); elem.dispatchEvent(new Event("change", {{bubbles: true}})); elem.dispatchEvent(new Event("blur"));' +fill_with_events = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("input", {{bubbles: true}})); elem.dispatchEvent(new Event("change", {{bubbles: true}})); elem.dispatchEvent(new Event("blur"));' +new_fill = 'elem.dispatchEvent(new Event("focus")); elem.value = {value}; window.__lemanaVpnFilled = true; elem.dispatchEvent(new Event("input", {{bubbles: true}})); elem.dispatchEvent(new Event("change", {{bubbles: true}})); elem.dispatchEvent(new Event("blur"));' if old_fill in src: src = src.replace(old_fill, new_fill) messages.append("input/change events") +elif fill_with_events in src: + src = src.replace(fill_with_events, new_fill) + messages.append("fill marker") + +old_click = 'var elem = document.querySelector({selector}); if (elem) {{ elem.dispatchEvent(new Event("focus")); elem.click(); }}' +new_click = 'var elem = document.querySelector({selector}); if (elem && !elem.disabled && elem.offsetParent !== null && window.__lemanaVpnFilled) {{ window.__lemanaVpnClicked = window.__lemanaVpnClicked || {{}}; var clickKey = location.href + "|" + {selector}; if (!window.__lemanaVpnClicked[clickKey]) {{ window.__lemanaVpnClicked[clickKey] = true; elem.dispatchEvent(new Event("focus")); elem.click(); }} }}' +if old_click in src: + src = src.replace(old_click, new_click) + messages.append("submit click guard") if "new RegExp" not in src: old_block = ''' script.setSourceCode( @@ -370,6 +384,26 @@ autoFill(); src = src.replace(old_block, new_block) messages.append("URL guard") +page_state = ''' if (window.__lemanaVpnPageHref !== location.href) {{ + window.__lemanaVpnPageHref = location.href; + window.__lemanaVpnFilled = false; + window.__lemanaVpnClicked = {{}}; + }} +''' +if "window.__lemanaVpnPageHref" not in src: + marker = ''' _afRun++; + + {get_selectors(rules, credentials)} +''' + if marker not in src: + print("Cannot apply page state guard patch: unsupported openconnect-lite source", file=sys.stderr) + sys.exit(1) + src = src.replace(marker, ''' _afRun++; +''' + page_state + ''' + {get_selectors(rules, credentials)} +''') + messages.append("page state guard") + if src != before: backup_dir.mkdir(parents=True, exist_ok=True) if not backup_file.exists(): @@ -656,6 +690,11 @@ if $CONFIGURE_KEYCHAIN_MODE; then exit 0 fi +if $PATCH_ONLY_MODE; then + _patch_oc + exit 0 +fi + if ! $JSON_MODE; then _module_status_human else diff --git a/tests/smoke.sh b/tests/smoke.sh index 6e3f1f7..e6cca06 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -34,6 +34,8 @@ printf '%s\n' "$status_json" | grep -q '"modules":' 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 '__lemanaVpnClicked' "$ROOT/bin/vpn-lemanapro.sh" status_text="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status)" printf '%s\n' "$status_text" | grep -q 'Modules:'