Добавь ручной и автоматический режимы VPN
This commit is contained in:
30
README.md
30
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.
|
||||
|
||||
## Удаление
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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" <<EOF
|
||||
# >>> lemana-vpn
|
||||
vpn() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" "\$@"; }
|
||||
vpn-auto() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --auto "\$@"; }
|
||||
vpn-manual() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --manual "\$@"; }
|
||||
vpn-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; }
|
||||
vpn-fix-dns() { sudo "$DNS_CLEANUP"; }
|
||||
# <<< lemana-vpn
|
||||
|
||||
@@ -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:'
|
||||
|
||||
Reference in New Issue
Block a user