Refine VPN installer and menu-bar app integration
This commit is contained in:
168
README.md
168
README.md
@@ -2,15 +2,15 @@
|
||||
|
||||
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
||||
|
||||
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.
|
||||
**По умолчанию:** ручной вход без сохранения паролей. Откроется окно корпоративного входа, а логин, пароль и одноразовый код вводятся вручную. Bitwarden, Touch ID, сохранение пароля, подстановка 2FA и автоматический вход включаются только явно.
|
||||
|
||||
**Credential sources:** `bitwarden` синхронизирует LDAP-пароль и TOTP seed из Bitwarden в macOS Keychain; `keychain` хранит LDAP-пароль и постоянный TOTP seed напрямую в macOS Keychain. Оба источника используют один и тот же runtime `openconnect-lite` для SSO/autofill.
|
||||
**Режимы входа:** `manual` ничего не сохраняет и ничего не подставляет; `saved-login` хранит LDAP логин/пароль в macOS Keychain и оставляет одноразовый код ручным; `saved-totp` дополнительно хранит постоянный код настройки 2FA; `auto` запускается только явно через `vpn-auto`/`--auto`.
|
||||
|
||||
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
||||
|
||||
- `openconnect` как VPN-клиент;
|
||||
- `openconnect-lite` для SAML SSO через Keycloak;
|
||||
- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;
|
||||
- опциональный Bitwarden CLI для LDAP-пароля и постоянного кода настройки 2FA;
|
||||
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
||||
- Swift Menu Bar app `LemanaVPN.app`;
|
||||
- безопасный DNS cleanup через root-owned wrapper;
|
||||
@@ -22,7 +22,7 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл
|
||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||
```
|
||||
|
||||
Если установка запущена из терминала, скрипт работает как интерактивный wizard: проверит, что уже стоит, спросит как хранить credentials, предложит нужные модули и проведёт через настройку.
|
||||
Если установка запущена из терминала, скрипт работает как простой wizard: объяснит, что будет установлено, по Enter оставит безопасный ручной режим и предложит дополнительные удобства только отдельным выбором.
|
||||
|
||||
После установки открой новый shell или выполни:
|
||||
|
||||
@@ -39,23 +39,30 @@ vpn
|
||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||
```
|
||||
|
||||
Первый важный вопрос — как хранить VPN credentials:
|
||||
Первый важный вопрос — как подключаться:
|
||||
|
||||
```text
|
||||
Как хранить VPN credentials?
|
||||
1) Bitwarden -> macOS Keychain
|
||||
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
||||
3) macOS Keychain: настрою вручную позже
|
||||
Как подключаться по умолчанию?
|
||||
|
||||
1) Вводить всё вручную (рекомендуется)
|
||||
Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.
|
||||
|
||||
2) Запомнить логин и пароль на этом Mac
|
||||
При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.
|
||||
|
||||
3) Настроить автоматический вход
|
||||
Нужно заранее сохранить пароль и постоянный код настройки 2FA.
|
||||
|
||||
Выбор [1/2/3, Enter=1]:
|
||||
```
|
||||
|
||||
Что означают варианты:
|
||||
|
||||
- `1` — использовать Bitwarden как sync-provider: installer поставит/проверит `bw`, а при запуске `vpn` CLI переложит LDAP password и TOTP seed из Bitwarden в macOS Keychain.
|
||||
- `2` — бесплатный Keychain-only путь: после установки CLI спросит LDAP password и постоянный TOTP seed или `otpauth://...secret=...`, затем сохранит их в macOS Keychain.
|
||||
- `3` — поставить CLI/app сейчас, а credentials настроить позже командой `vpn --configure-keychain`.
|
||||
- `1` — ничего не сохранять. Это самый понятный старт: при подключении открывается окно входа, всё вводится руками.
|
||||
- `2` — сохранить LDAP логин и пароль в macOS Keychain. Одноразовый код всё равно вводится руками.
|
||||
- `3` — продвинутый путь: сохранить полный набор данных для подстановки или подключить Bitwarden. Если не уверены, выбирайте `1`.
|
||||
|
||||
Дальше installer спросит только про системные модули: Touch ID для Bitwarden, sudoers, aliases, Swift Menu Bar app и автозапуск.
|
||||
Дальше installer спросит только про системные модули: sudoers, aliases, Swift Menu Bar app и автозапуск. Bitwarden и Touch ID появляются только если вы явно выбрали продвинутый путь.
|
||||
|
||||
Флаги остаются для CI, повторяемых установок и диагностики. Для обычной установки они не нужны.
|
||||
|
||||
@@ -69,7 +76,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh |
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||
| sh -s -- --non-interactive --credential-source keychain --configure-keychain
|
||||
| sh -s -- --non-interactive --auth-profile saved-login --configure-login
|
||||
```
|
||||
|
||||
Принудительно включить интерактивные вопросы:
|
||||
@@ -114,7 +121,7 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||
|
||||
```sh
|
||||
vpn --status
|
||||
Modules: ✅ core=ok, ✅ bitwarden=on, ✅ touchid=on, ✅ dns=on, ✅ app=on, ✅ autostart=on, ✅ patches=active, ✅ keychain=password:yes/totp_seed:yes
|
||||
Modules: ✅ core=ok, 🔐 auth_profile=manual, 🔐 credential_source=none, ⏭️ bitwarden=off, ⏭️ touchid=off, ✅ dns=on, ✅ app=on, ✅ autostart=on, ✅ patches=active, ✅ keychain=password:no/totp_seed:no
|
||||
VPN disconnected
|
||||
```
|
||||
|
||||
@@ -129,15 +136,17 @@ Emoji в human-выводе помогают быстро отличать но
|
||||
| Поле | Значение |
|
||||
| --- | --- |
|
||||
| `core=ok` | Есть `openconnect`, `openconnect-lite` и config |
|
||||
| `bitwarden=on` | Модуль включён и `bw` установлен |
|
||||
| `bitwarden=off` | Модуль отключён через `--without-bitwarden` или `LEMANA_VPN_USE_BITWARDEN=0` |
|
||||
| `auth_profile=manual/saved-login/saved-totp/auto` | Как CLI будет открывать окно входа и использовать сохранённые данные |
|
||||
| `credential_source=none/keychain/bitwarden` | Откуда брать сохранённые данные, если выбран режим с подстановкой |
|
||||
| `bitwarden=on` | Модуль явно включён и `bw` установлен |
|
||||
| `bitwarden=off` | Модуль отключён по умолчанию или через `--without-bitwarden` |
|
||||
| `bitwarden=missing` | Модуль включён, но `bw` не найден |
|
||||
| `touchid=on/off/missing` | Состояние Touch ID helper |
|
||||
| `dns=on/missing` | Наличие DNS cleanup wrapper |
|
||||
| `app=on/missing` | Установлен ли `~/Applications/LemanaVPN.app` |
|
||||
| `autostart=on/off` | Есть ли LaunchAgent для запуска приложения при логине |
|
||||
| `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` |
|
||||
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и TOTP seed в Keychain |
|
||||
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и постоянный код настройки 2FA в Keychain |
|
||||
|
||||
JSON-режим тоже отдаёт модульный статус:
|
||||
|
||||
@@ -163,20 +172,20 @@ Detected state:
|
||||
Menu Bar app: no
|
||||
LaunchAgent: no
|
||||
Keychain password: no
|
||||
Keychain TOTP seed: no
|
||||
Keychain 2FA setup: no
|
||||
```
|
||||
|
||||
Если доступен терминал, скрипт ведёт установку вопросами:
|
||||
|
||||
- выбрать credential source: Bitwarden sync, Keychain с вводом credentials сейчас, или Keychain с настройкой позже;
|
||||
- поставить ли Bitwarden CLI, если выбран Bitwarden и `bw` не найден;
|
||||
- собрать ли Touch ID helper, если выбран Bitwarden и helper не найден;
|
||||
- оставить ручной вход, сохранить только логин/пароль или настроить подстановку сохранённых данных;
|
||||
- поставить Bitwarden CLI только если выбран Bitwarden;
|
||||
- собрать Touch ID helper только если выбран Bitwarden;
|
||||
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
|
||||
- включить ли автозапуск Menu Bar app при логине;
|
||||
- настроить ли sudoers для `openconnect` и DNS cleanup;
|
||||
- добавить ли алиасы в `~/.zshrc`.
|
||||
|
||||
Флаги имеют приоритет над вопросами и нужны в основном для CI, диагностики или повторяемых unattended installs. Например, `--credential-source keychain --configure-keychain` сразу выберет Keychain-only flow, а `--no-shell` не будет предлагать алиасы.
|
||||
Флаги имеют приоритет над вопросами и нужны в основном для CI, диагностики или повторяемых unattended installs. Например, `--auth-profile saved-login --configure-login` сразу выберет сохранение логина/пароля, а `--no-shell` не будет предлагать алиасы.
|
||||
|
||||
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
|
||||
|
||||
@@ -211,54 +220,67 @@ LEMANA_VPN_NO_EMOJI=1 sh uninstall.sh
|
||||
- `openconnect-lite` config;
|
||||
- DNS cleanup wrapper.
|
||||
|
||||
### Как работают credential sources
|
||||
|
||||
У Lemana VPN есть два разных способа подготовить credentials, но один общий runtime-контракт: перед запуском SSO в macOS Keychain должны лежать LDAP-пароль и постоянный TOTP seed для `openconnect-lite`.
|
||||
### Как работают режимы входа
|
||||
|
||||
Keychain entries:
|
||||
|
||||
- service `openconnect-lite`, account `<LDAP username>` — корпоративный LDAP/domain пароль;
|
||||
- service `openconnect-lite`, account `totp/<LDAP username>` — постоянный TOTP seed.
|
||||
- service `openconnect-lite`, account `totp/<LDAP username>` — постоянный код настройки 2FA, нужен только для подстановки одноразового кода и auto mode.
|
||||
|
||||
`credential_source=bitwarden` — это sync-режим. CLI открывает Bitwarden vault, читает item `LM LDAP`, берёт из него LDAP password и TOTP seed, нормализует `otpauth://...secret=...` если нужно, затем записывает оба секрета в macOS Keychain. После этого подключение идёт так же, как в остальных режимах: `openconnect-lite` читает данные из Keychain, генерирует текущий одноразовый TOTP-код из seed и заполняет Keycloak форму.
|
||||
`auth_profile=manual` — режим по умолчанию. CLI не проверяет Keychain и Bitwarden, открывает видимое окно входа и ничего не подставляет. Логин, пароль и одноразовый код вводятся вручную.
|
||||
|
||||
`credential_source=keychain` — это бесплатный built-in режим без Bitwarden. Пользователь один раз запускает `vpn --configure-keychain` или установку с `--credential-source keychain --configure-keychain`, вводит LDAP password и постоянный TOTP seed. CLI сохраняет их напрямую в macOS Keychain и при следующих подключениях не спрашивает Bitwarden, master password или текущий OTP-код.
|
||||
`auth_profile=saved-login` — CLI берёт из Keychain только LDAP пароль и подставляет логин/пароль в видимое окно входа. Одноразовый код вводится руками. Настройка:
|
||||
|
||||
Важно: Lemana VPN не хранит и не принимает текущий 6-значный код как постоянную настройку. Такой код живёт около 30 секунд. Для автоматического SSO нужен именно seed: raw Base32 или `otpauth://totp/...?...secret=BASE32`.
|
||||
```sh
|
||||
vpn --configure-login
|
||||
```
|
||||
|
||||
Если запуск идёт из menu-bar app, интерактивного terminal prompt нет. Поэтому при пустом Keychain приложение покажет ошибку, а настройку нужно один раз выполнить в Terminal:
|
||||
`auth_profile=saved-totp` — CLI берёт LDAP пароль и постоянный код настройки 2FA из Keychain, подставляет поля в видимое окно входа, но submit не нажимает. Настройка:
|
||||
|
||||
```sh
|
||||
vpn --configure-keychain
|
||||
```
|
||||
|
||||
`auth_profile=auto` — старый автоматический путь: скрытый браузер, подстановка всех полей и submit. Запускается только явно:
|
||||
|
||||
```sh
|
||||
vpn-auto
|
||||
vpn --auto
|
||||
```
|
||||
|
||||
`credential_source=bitwarden` — это sync-provider. CLI открывает Bitwarden vault, читает item `LM LDAP`, берёт из него LDAP password и постоянный код настройки 2FA, нормализует `otpauth://...secret=...` если нужно, затем записывает оба секрета в macOS Keychain. Bitwarden не включён по умолчанию.
|
||||
|
||||
Важно: Lemana VPN не хранит и не принимает текущий 6-значный код как постоянную настройку. Такой код живёт около 30 секунд. Для автоматического SSO нужен именно постоянный secret: raw Base32 или `otpauth://totp/...?...secret=BASE32`.
|
||||
|
||||
Если запуск идёт из menu-bar app, интерактивного terminal prompt нет. Поэтому настройку сохранённого логина или полного набора данных нужно один раз выполнить в Terminal.
|
||||
|
||||
### Bitwarden
|
||||
|
||||
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
|
||||
Отключён по умолчанию. Его стоит включать только если у пользователя уже есть запись Bitwarden с корпоративным LDAP-паролем и постоянным кодом настройки 2FA.
|
||||
|
||||
TOTP seed — это постоянный секрет 2FA. Сам одноразовый TOTP-код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
|
||||
Постоянный код настройки 2FA — это секрет, который обычно показывается при ручной настройке authenticator. Сам одноразовый код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
|
||||
|
||||
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain.
|
||||
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password и постоянный код настройки 2FA из item `LM LDAP` и переложить их в macOS Keychain.
|
||||
|
||||
Выбрать Keychain вместо Bitwarden можно прямо в installer wizard: пункт `2` вводит LDAP password и TOTP seed сразу после установки, пункт `3` оставляет настройку на потом.
|
||||
Включить Bitwarden можно через продвинутый путь в installer wizard или флагами:
|
||||
|
||||
То же самое можно задать флагами для неинтерактивной установки:
|
||||
|
||||
```sh
|
||||
sh install.sh --non-interactive --credential-source keychain --configure-keychain
|
||||
sh install.sh --non-interactive --with-bitwarden --auth-profile saved-totp
|
||||
```
|
||||
|
||||
В этом режиме credentials нужно положить в Keychain вручную:
|
||||
Для Keychain-only режима без Bitwarden используйте:
|
||||
|
||||
```sh
|
||||
vpn-lemanapro.sh --configure-keychain
|
||||
sh install.sh --non-interactive --auth-profile saved-login --configure-login
|
||||
```
|
||||
|
||||
Если credentials уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. CLI явно напишет, что Bitwarden отключён и используются сохранённые LDAP password/TOTP seed из macOS Keychain.
|
||||
Если сохранённые данные уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. Для режима `saved-login` одноразовый код всё равно вводится вручную.
|
||||
|
||||
### Если Bitwarden нет
|
||||
|
||||
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
|
||||
Bitwarden не обязателен. Без него установка по умолчанию работает в полностью ручном режиме: откроется окно входа, а логин, пароль и одноразовый код вводятся руками.
|
||||
|
||||
Интерактивная установка:
|
||||
|
||||
@@ -266,27 +288,34 @@ Bitwarden не обязателен. Без него установка рабо
|
||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||
```
|
||||
|
||||
В первом вопросе выбери пункт `2`:
|
||||
В первом вопросе можно просто нажать Enter:
|
||||
|
||||
```text
|
||||
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
||||
1) Вводить всё вручную (рекомендуется)
|
||||
```
|
||||
|
||||
Это встроенный бесплатный путь: не нужен Bitwarden account, платный Bitwarden TOTP или внешний password manager. Setup prompt спросит корпоративный LDAP-пароль и постоянный TOTP seed. Seed можно вставить как raw Base32 или как `otpauth://totp/...?...secret=BASE32` URI.
|
||||
Это встроенный бесплатный путь: не нужен Bitwarden account, платный Bitwarden TOTP или внешний password manager.
|
||||
|
||||
Если хочется, чтобы логин и пароль подставлялись сами, выбери пункт `2` или выполни позже:
|
||||
|
||||
```sh
|
||||
vpn --configure-login
|
||||
```
|
||||
|
||||
Что понадобится:
|
||||
|
||||
- LDAP username;
|
||||
- LDAP password: корпоративный LDAP/domain пароль, не мастер-пароль Bitwarden;
|
||||
- TOTP secret из корпоративной 2FA настройки.
|
||||
- одноразовый код из корпоративной 2FA при каждом подключении.
|
||||
|
||||
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
|
||||
Постоянный код настройки 2FA нужен только для `vpn --configure-keychain`, `saved-totp` и `vpn-auto`.
|
||||
|
||||
Не вставляй текущий 6-значный authenticator code в `vpn --configure-keychain`. Lemana VPN сохраняет в Keychain постоянный TOTP seed, а `openconnect-lite` по нему генерирует свежие одноразовые коды во время каждого SSO login.
|
||||
Не вставляй текущий 6-значный authenticator code в `vpn --configure-keychain`. Lemana VPN сохраняет в Keychain постоянный код настройки 2FA, а `openconnect-lite` по нему генерирует свежие одноразовые коды во время каждого SSO login.
|
||||
|
||||
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal:
|
||||
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для сохранения пароля. Настройку saved-login или saved-totp нужно один раз выполнить в Terminal:
|
||||
|
||||
```sh
|
||||
vpn --configure-login
|
||||
vpn --configure-keychain
|
||||
```
|
||||
|
||||
@@ -295,13 +324,13 @@ vpn --configure-keychain
|
||||
1. Открой QR-код в приложении/на портале, где настраивалась 2FA.
|
||||
2. Найди режим ручной настройки, где показывается secret.
|
||||
3. Если доступен только QR, его нужно расшифровать любым локальным QR-сканером и взять параметр `secret`.
|
||||
4. Вставь secret в prompt `TOTP secret (BASE32...)`.
|
||||
4. Вставь secret в prompt `2FA setup secret BASE32`.
|
||||
|
||||
Если TOTP secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий TOTP-код на каждом входе.
|
||||
Если постоянный 2FA secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий одноразовый код на каждом входе.
|
||||
|
||||
### Touch ID
|
||||
|
||||
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
|
||||
Отключён по умолчанию и нужен только для Bitwarden-пути. Если пользователь выбрал Bitwarden, установщик может собрать `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и положить бинарник в `~/bin/keychain-fingerprint`.
|
||||
|
||||
Важно: этот helper показывает системный Touch ID prompt перед чтением мастер-пароля Bitwarden, но это не аппаратный Keychain ACL. Это удобный локальный гейт поверх записи Keychain.
|
||||
|
||||
@@ -321,6 +350,15 @@ sh install.sh --without-touchid
|
||||
|
||||
Приложение живёт в macOS status bar, запускает `~/bin/vpn-lemanapro.sh --json`, показывает состояние VPN, IP, оставшееся время сессии, health-check тоннеля и строку состояния модулей.
|
||||
|
||||
В отключённом состоянии меню показывает текущий режим установки:
|
||||
|
||||
- `Режим: полностью вручную` — доступна только обычная кнопка `Подключить`; логин, пароль и одноразовый код вводятся руками.
|
||||
- `Режим: логин и пароль сохранены` — доступен вход с подстановкой логина/пароля и ручной ввод одноразового кода.
|
||||
- `Режим: сохранённые данные без автоотправки` — доступны сохранённые данные, ручной fallback и явный automatic mode.
|
||||
- `Режим: автоматический вход` — обычная кнопка `Подключить` запускает настроенный auto-flow, а ручные fallback-кнопки остаются доступны.
|
||||
|
||||
Кнопки, для которых текущая установка не готова, не показываются. Например, если не сохранён постоянный код настройки 2FA, в меню не будет автоматического входа. Если рядом с режимом есть подсказка `Настройте в Terminal`, выполните указанную команду в Terminal и нажмите `Обновить статус модулей`.
|
||||
|
||||
Строка состояния модулей в меню приложения использует те же маркеры, что CLI: `✅` для готового модуля, `⏭️` для отключённого опционального модуля и `⚠️` для проблемы. Иконка строки тоже меняется: `checkmark.circle` для полностью готового набора и `exclamationmark.triangle` для неполной установки.
|
||||
|
||||
Если в меню видно `modules unavailable: update CLI`, значит запущенное приложение обращается к старому `~/bin/vpn-lemanapro.sh`, который ещё не умеет отдавать модульный статус. Повтори установку через `curl`; установщик обновит CLI и перезапустит уже запущенное `LemanaVPN.app`.
|
||||
@@ -352,9 +390,10 @@ open ~/Applications/LemanaVPN.app
|
||||
## Использование
|
||||
|
||||
```sh
|
||||
vpn # подключиться
|
||||
vpn-auto # автоматический режим: скрытый браузер, auto-fill и submit
|
||||
vpn-manual # ручной режим: видимый браузер, auto-fill без submit
|
||||
vpn # подключиться в настроенном режиме; fresh install = полностью вручную
|
||||
vpn-auto # явный автоматический режим: скрытый браузер, auto-fill и submit
|
||||
vpn --saved-login # подставить сохранённые логин/пароль, одноразовый код руками
|
||||
vpn-manual # видимый браузер, сохранённые данные auto-fill без submit
|
||||
vpn-manual-full # полностью ручной режим: видимый браузер без auto-fill и submit
|
||||
vpn --manual # то же самое без alias
|
||||
vpn --manual-full # то же самое без alias
|
||||
@@ -368,18 +407,19 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
|
||||
|
||||
Режимы подключения:
|
||||
|
||||
- `auto` — режим по умолчанию. Браузер скрытый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain, Keycloak форма заполняется и отправляется автоматически.
|
||||
- `manual` — браузер видимый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain и подставляются в поля, но кнопки входа не нажимаются. Нажимаешь сам после проверки формы.
|
||||
- `manual-full` — браузер видимый, auto-fill отключён полностью, проверки сохранённых LDAP/TOTP credentials пропускаются: поля Keycloak заполняешь и отправляешь сам.
|
||||
- `manual` — режим fresh install по умолчанию. Браузер видимый, auto-fill отключён полностью, проверки сохранённых LDAP/TOTP credentials пропускаются: поля Keycloak заполняешь и отправляешь сам.
|
||||
- `saved-login` — браузер видимый, логин/пароль берутся из Keychain, одноразовый код вводится руками, submit не нажимается.
|
||||
- `saved-totp` / `--manual` — браузер видимый, LDAP-пароль и код 2FA берутся из Keychain/Bitwarden и подставляются в поля, но кнопка входа не нажимается.
|
||||
- `auto` / `vpn-auto` — явный автоматический режим. Браузер скрытый, LDAP-пароль и код 2FA берутся из Keychain/Bitwarden, форма заполняется и отправляется автоматически.
|
||||
- `--manual-sso` оставлен как совместимый alias для `--manual`.
|
||||
- `--manual-no-autofill` оставлен как совместимый alias для `--manual-full`.
|
||||
|
||||
Первый запуск с Bitwarden:
|
||||
Первый запуск с Bitwarden, если он выбран явно:
|
||||
|
||||
1. CLI проверит `bw`.
|
||||
2. Если vault locked, попросит мастер-пароль.
|
||||
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
|
||||
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP seed в Keychain.
|
||||
4. Достанет `LM LDAP`, запишет LDAP-пароль и постоянный код настройки 2FA в Keychain.
|
||||
5. Запустит `openconnect-lite` и пройдёт Keycloak SSO.
|
||||
|
||||
## Настройка
|
||||
@@ -388,17 +428,19 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
|
||||
|
||||
```sh
|
||||
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
|
||||
LEMANA_VPN_AUTH_PROFILE="manual"
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE="none"
|
||||
LEMANA_VPN_BW_ITEM="LM LDAP"
|
||||
LEMANA_VPN_USE_BITWARDEN="1"
|
||||
LEMANA_VPN_USE_TOUCHID="1"
|
||||
LEMANA_VPN_USE_BITWARDEN="0"
|
||||
LEMANA_VPN_USE_TOUCHID="0"
|
||||
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||
```
|
||||
|
||||
Для бесплатного Keychain-only источника:
|
||||
Для режима с сохранённым логином и паролем:
|
||||
|
||||
```sh
|
||||
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
|
||||
LEMANA_VPN_AUTH_PROFILE="saved-login"
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
|
||||
LEMANA_VPN_USE_BITWARDEN="0"
|
||||
LEMANA_VPN_USE_TOUCHID="0"
|
||||
@@ -421,7 +463,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||
- password: LDAP пароль;
|
||||
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
|
||||
|
||||
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный TOTP secret, из которого коды генерируются автоматически.
|
||||
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный 2FA secret, из которого коды генерируются автоматически.
|
||||
|
||||
## Почему DNS wrapper, а не wildcard sudoers
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ struct VPNMenuView: View {
|
||||
Button("Обновить статус модулей") {
|
||||
vpnManager.refreshStatus()
|
||||
}
|
||||
Button("Изменить логин и пароль") {
|
||||
vpnManager.configureLogin()
|
||||
}
|
||||
Divider()
|
||||
Button("Открыть логи") {
|
||||
let logPath = FileManager.default.homeDirectoryForCurrentUser
|
||||
@@ -91,19 +94,18 @@ struct VPNMenuView: View {
|
||||
Group {
|
||||
Label("VPN отключён", systemImage: "circle")
|
||||
.disabled(true)
|
||||
Label(vpnManager.installModeLabel, systemImage: "slider.horizontal.3")
|
||||
.disabled(true)
|
||||
if let setupHint = vpnManager.installSetupHint {
|
||||
Label(setupHint, systemImage: "info.circle")
|
||||
.disabled(true)
|
||||
}
|
||||
Divider()
|
||||
Button("Подключить автоматически") {
|
||||
vpnManager.connect(mode: .auto)
|
||||
ForEach(vpnManager.availableLaunchModes) { mode in
|
||||
Button(mode.menuTitle) {
|
||||
vpnManager.connect(mode: mode)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut("c")
|
||||
Button("Подключить вручную") {
|
||||
vpnManager.connect(mode: .manual)
|
||||
}
|
||||
.keyboardShortcut("m")
|
||||
Button("Подключить полностью вручную") {
|
||||
vpnManager.connect(mode: .manualFull)
|
||||
}
|
||||
.keyboardShortcut("f")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ struct VPNEvent: Decodable {
|
||||
var reason: String?
|
||||
var message: String?
|
||||
var source: String?
|
||||
var profile: String?
|
||||
var auth_profile: String?
|
||||
var tier: Int?
|
||||
var modules: ModuleStatus?
|
||||
}
|
||||
@@ -34,9 +36,18 @@ struct ModuleStatus: Decodable {
|
||||
|
||||
struct Credentials: Decodable {
|
||||
var source: String
|
||||
var auth_profile: String?
|
||||
var login_ready: Bool?
|
||||
var keychain_ready: Bool
|
||||
}
|
||||
|
||||
struct AppMenu: Decodable {
|
||||
var install_label: String
|
||||
var configured_mode: String
|
||||
var available_modes: [String]
|
||||
var setup_hint: String?
|
||||
}
|
||||
|
||||
struct Keychain: Decodable {
|
||||
var password: Bool
|
||||
var totp_seed: Bool
|
||||
@@ -58,6 +69,7 @@ struct ModuleStatus: Decodable {
|
||||
|
||||
var core: Core
|
||||
var credentials: Credentials?
|
||||
var app_menu: AppMenu?
|
||||
var bitwarden: ToggleModule
|
||||
var touchid: ToggleModule
|
||||
var keychain: Keychain
|
||||
@@ -69,7 +81,16 @@ struct ModuleStatus: Decodable {
|
||||
let coreReady = core.openconnect && core.openconnect_lite && core.openconnect_lite_config
|
||||
let bitwardenReady = !bitwarden.enabled || bitwarden.installed
|
||||
let touchReady = !touchid.enabled || touchid.installed
|
||||
let keychainReady = keychain.password && keychain.totp_seed
|
||||
let profile = credentials?.auth_profile ?? "manual"
|
||||
let keychainReady: Bool
|
||||
switch profile {
|
||||
case "manual":
|
||||
keychainReady = true
|
||||
case "saved-login":
|
||||
keychainReady = keychain.password
|
||||
default:
|
||||
keychainReady = keychain.password && keychain.totp_seed
|
||||
}
|
||||
let appReady = app?.installed ?? true
|
||||
|
||||
return !coreReady
|
||||
@@ -87,14 +108,23 @@ struct ModuleStatus: Decodable {
|
||||
|
||||
var summary: String {
|
||||
let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "✅ core" : "⚠️ core"
|
||||
let credentialState = credentials.map { "🔐 \($0.source)" } ?? "🔐 legacy"
|
||||
let credentialState = credentials.map { "🔐 \($0.auth_profile ?? "manual")/\($0.source)" } ?? "🔐 legacy"
|
||||
let bwState = bitwarden.enabled ? (bitwarden.installed ? "✅ bw" : "⚠️ bw") : "⏭️ bw"
|
||||
let touchState = touchid.enabled ? (touchid.installed ? "✅ touch" : "⚠️ touch") : "⏭️ touch"
|
||||
let dnsState = dns_cleanup.installed ? "✅ dns" : "⚠️ dns"
|
||||
let appState = app.map { $0.installed ? "✅ app" : "⚠️ app" } ?? "❔ app"
|
||||
let autostartState = app.map { $0.autostart ? "✅ autostart" : "⏭️ autostart" } ?? "❔ autostart"
|
||||
let patchState = patches.active ? "✅ patches" : "⚠️ patches"
|
||||
let keychainState = "\(keychain.password && keychain.totp_seed ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
||||
let keychainReady: Bool
|
||||
switch credentials?.auth_profile ?? "manual" {
|
||||
case "manual":
|
||||
keychainReady = true
|
||||
case "saved-login":
|
||||
keychainReady = keychain.password
|
||||
default:
|
||||
keychainReady = keychain.password && keychain.totp_seed
|
||||
}
|
||||
let keychainState = "\(keychainReady ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
||||
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
||||
}
|
||||
}
|
||||
@@ -119,18 +149,34 @@ enum VPNState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
enum VPNLaunchMode: String {
|
||||
case auto
|
||||
case manual
|
||||
case manualFull
|
||||
enum VPNLaunchMode: String, Identifiable {
|
||||
case configured = "configured"
|
||||
case auto = "auto"
|
||||
case savedLogin = "saved-login"
|
||||
case manual = "saved-totp"
|
||||
case manualFull = "manual-full"
|
||||
|
||||
var cliArgument: String {
|
||||
var id: String { rawValue }
|
||||
|
||||
var cliArgument: String? {
|
||||
switch self {
|
||||
case .configured: return nil
|
||||
case .auto: return "--auto"
|
||||
case .savedLogin: return "--saved-login"
|
||||
case .manual: return "--manual"
|
||||
case .manualFull: return "--manual-full"
|
||||
}
|
||||
}
|
||||
|
||||
var menuTitle: String {
|
||||
switch self {
|
||||
case .configured: return "Подключить"
|
||||
case .auto: return "Подключить автоматически"
|
||||
case .savedLogin: return "Подставить только логин и пароль"
|
||||
case .manual: return "Подключить с сохранёнными данными"
|
||||
case .manualFull: return "Подключить полностью вручную"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -140,6 +186,7 @@ class VPNManager: ObservableObject {
|
||||
@Published var tunnelHealthy: Bool = true
|
||||
@Published var moduleSummary: String = "modules loading..."
|
||||
@Published var moduleStatusSystemImage: String = "hourglass"
|
||||
@Published var moduleStatus: ModuleStatus?
|
||||
|
||||
private var process: Process?
|
||||
private var outputPipe: Pipe?
|
||||
@@ -153,7 +200,7 @@ class VPNManager: ObservableObject {
|
||||
private var autoReconnectAttempts: Int = 0
|
||||
private var reconnectTimer: Timer?
|
||||
private var consecutiveHealthFailures: Int = 0
|
||||
private var currentLaunchMode: VPNLaunchMode = .auto
|
||||
private var currentLaunchMode: VPNLaunchMode = .configured
|
||||
|
||||
private let healthCheckInterval: TimeInterval = 10
|
||||
private let maxAutoReconnectAttempts: Int = 3
|
||||
@@ -191,6 +238,28 @@ class VPNManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var installModeLabel: String {
|
||||
moduleStatus?.app_menu?.install_label ?? "Режим: проверяю установку"
|
||||
}
|
||||
|
||||
var installSetupHint: String? {
|
||||
if let hint = moduleStatus?.app_menu?.setup_hint {
|
||||
return hint
|
||||
}
|
||||
if moduleStatus != nil, moduleStatus?.app_menu == nil {
|
||||
return "Обновите CLI: sh install.sh"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var availableLaunchModes: [VPNLaunchMode] {
|
||||
guard let menu = moduleStatus?.app_menu else {
|
||||
return [.configured]
|
||||
}
|
||||
let modes = menu.available_modes.compactMap(VPNLaunchMode.init(rawValue:))
|
||||
return modes.isEmpty ? [.configured] : modes
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
let proc = Process()
|
||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||
@@ -211,6 +280,7 @@ class VPNManager: ObservableObject {
|
||||
guard !lastLine.isEmpty, let jsonData = lastLine.data(using: .utf8) else {
|
||||
self.moduleSummary = "modules unavailable"
|
||||
self.moduleStatusSystemImage = "questionmark.circle"
|
||||
self.moduleStatus = nil
|
||||
self.log("[modules] status refresh returned no JSON output")
|
||||
return
|
||||
}
|
||||
@@ -221,6 +291,7 @@ class VPNManager: ObservableObject {
|
||||
} catch {
|
||||
self.moduleSummary = "modules unavailable"
|
||||
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
||||
self.moduleStatus = nil
|
||||
let compact = text.replacingOccurrences(of: "\n", with: "\\n")
|
||||
let preview = compact.count > 500 ? String(compact.prefix(500)) + "..." : compact
|
||||
self.log("[modules] status decode failed: \(error.localizedDescription); output=\(preview)")
|
||||
@@ -230,12 +301,14 @@ class VPNManager: ObservableObject {
|
||||
guard let modules = response.modules else {
|
||||
self.moduleSummary = "modules unavailable: update CLI"
|
||||
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
||||
self.moduleStatus = nil
|
||||
self.log("[modules] status has no modules field; reinstall CLI with install.sh")
|
||||
return
|
||||
}
|
||||
|
||||
self.moduleSummary = modules.summary
|
||||
self.moduleStatusSystemImage = modules.systemImage
|
||||
self.moduleStatus = modules
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +319,7 @@ class VPNManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func connect(mode: VPNLaunchMode = .auto) {
|
||||
func connect(mode: VPNLaunchMode = .configured) {
|
||||
guard !isRunning else {
|
||||
log("connect() called but process already running")
|
||||
return
|
||||
@@ -264,7 +337,11 @@ class VPNManager: ObservableObject {
|
||||
|
||||
let proc = Process()
|
||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||
proc.arguments = ["-l", scriptPath, "--json", mode.cliArgument]
|
||||
var arguments = ["-l", scriptPath, "--json"]
|
||||
if let cliArgument = mode.cliArgument {
|
||||
arguments.append(cliArgument)
|
||||
}
|
||||
proc.arguments = arguments
|
||||
proc.environment = processEnvironment()
|
||||
|
||||
let stdoutPipe = Pipe()
|
||||
@@ -337,6 +414,10 @@ class VPNManager: ObservableObject {
|
||||
proc.interrupt()
|
||||
}
|
||||
|
||||
func configureLogin() {
|
||||
openTerminalCommand(name: "configure-login", command: "\(shellQuote(scriptPath)) --configure-login")
|
||||
}
|
||||
|
||||
func quit() {
|
||||
log("-- App quit --")
|
||||
userInitiatedDisconnect = true
|
||||
@@ -357,6 +438,7 @@ class VPNManager: ObservableObject {
|
||||
if let modules = event.modules {
|
||||
moduleSummary = modules.summary
|
||||
moduleStatusSystemImage = modules.systemImage
|
||||
moduleStatus = modules
|
||||
}
|
||||
|
||||
log("[event] \(event.event)" + {
|
||||
@@ -367,6 +449,7 @@ class VPNManager: ObservableObject {
|
||||
if let a = event.attempt { extras.append("attempt=\(a)") }
|
||||
if let m = event.message { extras.append("msg=\(m)") }
|
||||
if let r = event.reason { extras.append("reason=\(r)") }
|
||||
if let p = event.auth_profile ?? event.profile { extras.append("profile=\(p)") }
|
||||
return extras.isEmpty ? "" : " (\(extras.joined(separator: ", ")))"
|
||||
}())
|
||||
|
||||
@@ -383,6 +466,26 @@ class VPNManager: ObservableObject {
|
||||
log(" Credential source: \(source)")
|
||||
}
|
||||
return
|
||||
case "auth_profile":
|
||||
if let message = event.message {
|
||||
log(" \(message)")
|
||||
} else if let profile = event.profile {
|
||||
log(" Auth profile: \(profile)")
|
||||
}
|
||||
return
|
||||
case "manual_sso", "auto_sso":
|
||||
let profile = event.auth_profile ?? event.profile ?? "configured"
|
||||
log(" SSO mode: \(profile)")
|
||||
return
|
||||
case "username_required", "username_saved":
|
||||
if let message = event.message {
|
||||
log(" \(message)")
|
||||
} else if event.event == "username_required" {
|
||||
log(" LDAP username is missing; configure it in Terminal")
|
||||
} else {
|
||||
log(" LDAP username saved")
|
||||
}
|
||||
return
|
||||
case "bw_cached":
|
||||
state = .unlocking(tier: "cached")
|
||||
case "bw_touchid":
|
||||
@@ -446,6 +549,46 @@ class VPNManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func openTerminalCommand(name: String, command: String) {
|
||||
let stateDir = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".local/state/vpn-lemanapro")
|
||||
let commandURL = stateDir.appendingPathComponent("\(name).command")
|
||||
let script = """
|
||||
#!/bin/zsh
|
||||
\(command)
|
||||
status=$?
|
||||
printf '\\n'
|
||||
if [ "$status" -eq 0 ]; then
|
||||
printf 'Логин и пароль обновлены.\\n'
|
||||
else
|
||||
printf 'Команда завершилась с ошибкой: %s\\n' "$status"
|
||||
fi
|
||||
printf 'Нажмите Enter, чтобы закрыть окно...'
|
||||
read -r _
|
||||
exit "$status"
|
||||
"""
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: stateDir, withIntermediateDirectories: true)
|
||||
try script.write(to: commandURL, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: commandURL.path)
|
||||
|
||||
let proc = Process()
|
||||
proc.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
||||
proc.arguments = ["-a", "Terminal", commandURL.path]
|
||||
try proc.run()
|
||||
log("Opened Terminal credential setup: \(commandURL.path)")
|
||||
} catch {
|
||||
log("Credential setup launch failed: \(error.localizedDescription)")
|
||||
lastError = error.localizedDescription
|
||||
state = .error(message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func shellQuote(_ value: String) -> String {
|
||||
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
||||
}
|
||||
|
||||
private func handleTermination(exitCode: Int32) {
|
||||
log("-- Process terminated (exit=\(exitCode), userInitiated=\(userInitiatedDisconnect)) --")
|
||||
stopTimer()
|
||||
|
||||
@@ -7,6 +7,7 @@ OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||
|
||||
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
|
||||
_ENV_LEMANA_VPN_BW_ITEM="${LEMANA_VPN_BW_ITEM+x}${LEMANA_VPN_BW_ITEM-}"
|
||||
_ENV_LEMANA_VPN_AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE+x}${LEMANA_VPN_AUTH_PROFILE-}"
|
||||
_ENV_LEMANA_VPN_USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN+x}${LEMANA_VPN_USE_BITWARDEN-}"
|
||||
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
|
||||
_ENV_LEMANA_VPN_USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID+x}${LEMANA_VPN_USE_TOUCHID-}"
|
||||
@@ -19,6 +20,7 @@ fi
|
||||
|
||||
[[ "${_ENV_LEMANA_VPN_USERNAME:0:1}" == "x" ]] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME:1}"
|
||||
[[ "${_ENV_LEMANA_VPN_BW_ITEM:0:1}" == "x" ]] && LEMANA_VPN_BW_ITEM="${_ENV_LEMANA_VPN_BW_ITEM:1}"
|
||||
[[ "${_ENV_LEMANA_VPN_AUTH_PROFILE:0:1}" == "x" ]] && LEMANA_VPN_AUTH_PROFILE="${_ENV_LEMANA_VPN_AUTH_PROFILE:1}"
|
||||
[[ "${_ENV_LEMANA_VPN_USE_BITWARDEN:0:1}" == "x" ]] && LEMANA_VPN_USE_BITWARDEN="${_ENV_LEMANA_VPN_USE_BITWARDEN:1}"
|
||||
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
|
||||
[[ "${_ENV_LEMANA_VPN_USE_TOUCHID:0:1}" == "x" ]] && LEMANA_VPN_USE_TOUCHID="${_ENV_LEMANA_VPN_USE_TOUCHID:1}"
|
||||
@@ -30,19 +32,28 @@ OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
|
||||
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||
KC_USERNAME="${LEMANA_VPN_USERNAME:-}"
|
||||
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
||||
AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE:-manual}"
|
||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
|
||||
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
|
||||
if [[ "${LEMANA_VPN_USE_BITWARDEN:-1}" == "1" ]]; then
|
||||
if [[ "${LEMANA_VPN_USE_BITWARDEN:-0}" == "1" ]]; then
|
||||
CREDENTIAL_SOURCE="bitwarden"
|
||||
else
|
||||
CREDENTIAL_SOURCE="keychain"
|
||||
CREDENTIAL_SOURCE="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$CREDENTIAL_SOURCE" in
|
||||
bitwarden|keychain) ;;
|
||||
case "$AUTH_PROFILE" in
|
||||
manual|saved-login|saved-totp|auto) ;;
|
||||
*)
|
||||
printf 'Unknown credential source: %s. Use bitwarden or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
||||
printf 'Unknown auth profile: %s. Use manual, saved-login, saved-totp, or auto.\n' "$AUTH_PROFILE" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$CREDENTIAL_SOURCE" in
|
||||
none|bitwarden|keychain) ;;
|
||||
*)
|
||||
printf 'Unknown credential source: %s. Use none, bitwarden, or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -52,8 +63,8 @@ if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||
else
|
||||
USE_BITWARDEN="0"
|
||||
fi
|
||||
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}"
|
||||
if [[ "$CREDENTIAL_SOURCE" == "keychain" ]]; then
|
||||
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-0}"
|
||||
if [[ "$CREDENTIAL_SOURCE" != "bitwarden" ]]; then
|
||||
USE_TOUCHID="0"
|
||||
fi
|
||||
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
||||
@@ -69,38 +80,45 @@ PATCH_BACKUP_DIR="${LEMANA_VPN_PATCH_BACKUP_DIR:-$CONFIG_DIR/patch-backups}"
|
||||
CONNECT_LOG_DIR="${LEMANA_VPN_LOG_DIR:-$HOME/Library/Logs}"
|
||||
CONNECT_LOG_FILE="${LEMANA_VPN_CONNECT_LOG:-$CONNECT_LOG_DIR/LemanaVPN-openconnect-lite.log}"
|
||||
CONNECT_WAIT_SECONDS="${LEMANA_VPN_CONNECT_WAIT_SECONDS:-20}"
|
||||
OC_RUNTIME_XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
MANUAL_XDG_CONFIG_HOME=""
|
||||
|
||||
DEBUG=false
|
||||
JSON_MODE=false
|
||||
STATUS_MODE=false
|
||||
CONFIGURE_LOGIN_MODE=false
|
||||
CONFIGURE_KEYCHAIN_MODE=false
|
||||
PATCH_ONLY_MODE=false
|
||||
CONNECT_MODE="${LEMANA_VPN_MODE:-auto}"
|
||||
CONNECT_MODE="${LEMANA_VPN_MODE:-}"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--debug) DEBUG=true ;;
|
||||
--json) JSON_MODE=true ;;
|
||||
--status) STATUS_MODE=true ;;
|
||||
--configure-login) CONFIGURE_LOGIN_MODE=true ;;
|
||||
--configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;;
|
||||
--patch-only) PATCH_ONLY_MODE=true ;;
|
||||
--auto|auto) CONNECT_MODE="auto" ;;
|
||||
--manual|manual|--manual-sso) CONNECT_MODE="manual" ;;
|
||||
--manual-full|manual-full|--manual-no-autofill) CONNECT_MODE="manual-full" ;;
|
||||
--auto|auto) AUTH_PROFILE="auto"; CONNECT_MODE="auto" ;;
|
||||
--manual|manual|--manual-sso) AUTH_PROFILE="saved-totp"; CONNECT_MODE="saved-totp" ;;
|
||||
--saved-login|saved-login|--login-only) AUTH_PROFILE="saved-login"; CONNECT_MODE="saved-login" ;;
|
||||
--manual-full|manual-full|--manual-no-autofill) AUTH_PROFILE="manual"; CONNECT_MODE="manual-full" ;;
|
||||
--help|-h)
|
||||
cat <<'HELP'
|
||||
Usage: vpn-lemanapro.sh [--auto|--manual|--manual-full] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
|
||||
Usage: vpn-lemanapro.sh [--auto|--manual|--saved-login|--manual-full] [--debug] [--json] [--status] [--configure-login] [--configure-keychain] [--patch-only]
|
||||
|
||||
--status Show current VPN status without connecting
|
||||
--status --json Show current VPN status as JSON
|
||||
--auto Hidden browser, auto-fill and auto-submit (default)
|
||||
--manual Visible browser, auto-fill fields, do not press submit
|
||||
--manual-full Visible browser, no auto-fill, do not press submit
|
||||
--auto Hidden browser, auto-fill and auto-submit
|
||||
--manual Visible browser, saved fields auto-filled, do not press submit
|
||||
--saved-login Visible browser, saved username/password only, OTP manual
|
||||
--manual-full Visible browser, no auto-fill, do not press submit (default on fresh install)
|
||||
--manual-no-autofill Compatibility alias for --manual-full
|
||||
--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 Configure the keychain credential source: LDAP password plus permanent TOTP seed or otpauth:// URI
|
||||
--configure-login Save LDAP username/password in macOS Keychain; OTP remains manual
|
||||
--configure-keychain Save LDAP password plus permanent 2FA setup code or otpauth:// URI
|
||||
--patch-only Apply openconnect-lite runtime patches and exit
|
||||
HELP
|
||||
exit 0
|
||||
@@ -109,9 +127,23 @@ HELP
|
||||
done
|
||||
|
||||
case "$CONNECT_MODE" in
|
||||
auto|manual|manual-full) ;;
|
||||
"")
|
||||
;;
|
||||
manual-full)
|
||||
AUTH_PROFILE="manual"
|
||||
;;
|
||||
manual)
|
||||
AUTH_PROFILE="saved-totp"
|
||||
;;
|
||||
saved-login|saved-totp|auto)
|
||||
AUTH_PROFILE="$CONNECT_MODE"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$AUTH_PROFILE" in
|
||||
manual|saved-login|saved-totp|auto) ;;
|
||||
*)
|
||||
printf 'Unknown VPN mode: %s. Use --auto, --manual, or --manual-full.\n' "$CONNECT_MODE" >&2
|
||||
printf 'Unknown auth profile: %s. Use manual, saved-login, saved-totp, or auto.\n' "$AUTH_PROFILE" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -134,11 +166,56 @@ _clear_status() {
|
||||
_write_status "{\"pid\":$$,\"state\":\"disconnected\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||
}
|
||||
|
||||
_cleanup_manual_openconnect_config() {
|
||||
if [[ -n "${MANUAL_XDG_CONFIG_HOME:-}" && -d "$MANUAL_XDG_CONFIG_HOME" ]]; then
|
||||
rm -rf "$MANUAL_XDG_CONFIG_HOME"
|
||||
MANUAL_XDG_CONFIG_HOME=""
|
||||
fi
|
||||
}
|
||||
|
||||
_prepare_manual_openconnect_config() {
|
||||
local source_config temp_root temp_config_dir
|
||||
source_config="$OC_CONFIG_DIR/config.toml"
|
||||
temp_root="$(mktemp -d "${TMPDIR:-/tmp}/lemana-vpn-manual-xdg.XXXXXX")"
|
||||
temp_config_dir="$temp_root/openconnect-lite"
|
||||
mkdir -p "$temp_config_dir"
|
||||
|
||||
if [[ -f "$source_config" ]]; then
|
||||
CONFIG_SRC="$source_config" CONFIG_DST="$temp_config_dir/config.toml" python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
src = Path(os.environ["CONFIG_SRC"])
|
||||
dst = Path(os.environ["CONFIG_DST"])
|
||||
|
||||
lines = src.read_text().splitlines()
|
||||
rendered = []
|
||||
skip_credentials = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped == "[credentials]" or stripped.startswith("[credentials."):
|
||||
skip_credentials = True
|
||||
continue
|
||||
if skip_credentials and stripped.startswith("[") and stripped.endswith("]"):
|
||||
skip_credentials = False
|
||||
if not skip_credentials:
|
||||
rendered.append(line)
|
||||
|
||||
dst.write_text("\n".join(rendered).rstrip() + "\n")
|
||||
dst.chmod(0o600)
|
||||
PY
|
||||
fi
|
||||
|
||||
MANUAL_XDG_CONFIG_HOME="$temp_root"
|
||||
OC_RUNTIME_XDG_CONFIG_HOME="$temp_root"
|
||||
}
|
||||
|
||||
_prepare_connection_log() {
|
||||
mkdir -p "$CONNECT_LOG_DIR"
|
||||
{
|
||||
printf '\n==== Lemana VPN openconnect-lite session %s ====\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf 'Command: %s --browser-display-mode hidden\n' "$OC_BIN"
|
||||
printf 'Command: %s\n' "$OC_BIN"
|
||||
} >> "$CONNECT_LOG_FILE"
|
||||
chmod 600 "$CONNECT_LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
@@ -222,11 +299,65 @@ _keychain_has() {
|
||||
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
_app_menu_json() {
|
||||
local login_ready="$1" keychain_ready="$2"
|
||||
local install_label configured_mode available_modes setup_hint_json
|
||||
|
||||
configured_mode="$AUTH_PROFILE"
|
||||
setup_hint_json="null"
|
||||
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
install_label="Режим: полностью вручную"
|
||||
available_modes='"configured"'
|
||||
;;
|
||||
saved-login)
|
||||
if [[ "$login_ready" == "true" ]]; then
|
||||
install_label="Режим: логин и пароль сохранены"
|
||||
available_modes='"configured","manual-full"'
|
||||
else
|
||||
install_label="Режим: логин и пароль не настроены"
|
||||
available_modes='"manual-full"'
|
||||
setup_hint_json='"Настройте в Terminal: vpn --configure-login"'
|
||||
fi
|
||||
;;
|
||||
saved-totp)
|
||||
if [[ "$keychain_ready" == "true" ]]; then
|
||||
install_label="Режим: сохранённые данные без автоотправки"
|
||||
available_modes='"configured","saved-login","manual-full","auto"'
|
||||
else
|
||||
install_label="Режим: сохранённые данные не настроены"
|
||||
available_modes='"manual-full"'
|
||||
setup_hint_json='"Настройте в Terminal: vpn --configure-keychain"'
|
||||
fi
|
||||
;;
|
||||
auto)
|
||||
if [[ "$keychain_ready" == "true" ]]; then
|
||||
install_label="Режим: автоматический вход"
|
||||
available_modes='"configured","saved-totp","saved-login","manual-full"'
|
||||
else
|
||||
install_label="Режим: автоматический вход не настроен"
|
||||
available_modes='"manual-full"'
|
||||
setup_hint_json='"Настройте в Terminal: vpn --configure-keychain"'
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
install_label="Режим: неизвестен"
|
||||
configured_mode="manual"
|
||||
available_modes='"configured"'
|
||||
setup_hint_json='"Обновите Lemana VPN через install.sh"'
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '{"install_label":"%s","configured_mode":"%s","available_modes":[%s],"setup_hint":%s}' \
|
||||
"$install_label" "$configured_mode" "$available_modes" "$setup_hint_json"
|
||||
}
|
||||
|
||||
_module_status_json() {
|
||||
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
|
||||
local app_installed app_autostart
|
||||
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
|
||||
local credential_keychain_ready
|
||||
local credential_keychain_ready credential_login_ready app_menu_json
|
||||
openconnect_installed="$(_module_bool command -v openconnect)"
|
||||
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
|
||||
bitwarden_installed="$(_module_bool command -v bw)"
|
||||
@@ -241,14 +372,19 @@ _module_status_json() {
|
||||
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
|
||||
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
|
||||
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
|
||||
credential_login_ready="$([[ "$keychain_password" == "true" ]] && printf true || printf false)"
|
||||
app_menu_json="$(_app_menu_json "$credential_login_ready" "$credential_keychain_ready")"
|
||||
|
||||
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","keychain_ready":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","auth_profile":"%s","login_ready":%s,"keychain_ready":%s},"app_menu":%s,"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||
"$openconnect_installed" \
|
||||
"$openconnect_lite_installed" \
|
||||
"$config_present" \
|
||||
"$oc_config_present" \
|
||||
"$CREDENTIAL_SOURCE" \
|
||||
"$AUTH_PROFILE" \
|
||||
"$credential_login_ready" \
|
||||
"$credential_keychain_ready" \
|
||||
"$app_menu_json" \
|
||||
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
|
||||
"$bitwarden_installed" \
|
||||
"$BW_ITEM_NAME" \
|
||||
@@ -292,7 +428,7 @@ _module_status_human() {
|
||||
app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
||||
app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")"
|
||||
|
||||
printf 'Modules: %s %s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$CREDENTIAL_SOURCE"
|
||||
printf 'Modules: %s %s, 🔐 auth_profile=%s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$AUTH_PROFILE" "$CREDENTIAL_SOURCE"
|
||||
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
|
||||
printf ', '
|
||||
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
|
||||
@@ -305,7 +441,7 @@ _module_status_human() {
|
||||
"$([[ "$app_autostart" == "true" ]] && printf on || printf off)" \
|
||||
"$([[ "$patches_active" == "true" ]] && printf '✅' || printf '⚠️')" \
|
||||
"$([[ "$patches_active" == "true" ]] && printf active || printf pending)" \
|
||||
"$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf '✅' || printf '⚠️')" \
|
||||
"$([[ "$AUTH_PROFILE" == "manual" || "$keychain_password" == "true" && "$AUTH_PROFILE" == "saved-login" || "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf '✅' || printf '⚠️')" \
|
||||
"$([[ "$keychain_password" == "true" ]] && printf yes || printf no)" \
|
||||
"$([[ "$keychain_totp_seed" == "true" ]] && printf yes || printf no)"
|
||||
}
|
||||
@@ -526,6 +662,8 @@ canonical_selectors = '''def get_selectors(rules, credentials):
|
||||
f"""var elem = document.querySelector({selector}); if (elem) {{ return; }}"""
|
||||
)
|
||||
elif rule.fill:
|
||||
if rule.fill == "totp" and os.environ.get("LEMANA_VPN_AUTOFILL_TOTP", "1") == "0":
|
||||
continue
|
||||
value = json.dumps(getattr(credentials, rule.fill, None))
|
||||
if value:
|
||||
statements.append(
|
||||
@@ -641,7 +779,7 @@ if not value:
|
||||
sys.exit(0)
|
||||
|
||||
if not re.fullmatch(r"[A-Z2-7]+=*", value):
|
||||
print("Invalid TOTP seed. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
||||
print("Invalid 2FA setup secret. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(value)
|
||||
@@ -753,7 +891,7 @@ _configure_keychain() {
|
||||
printf 'Manual LDAP credentials setup for Lemana VPN\n'
|
||||
printf 'User: %s\n' "$KC_USERNAME"
|
||||
printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n'
|
||||
printf 'TOTP seed: permanent BASE32 secret from 2FA setup, not the current 6-digit code.\n'
|
||||
printf '2FA setup secret: permanent BASE32 secret from authenticator setup, not the current 6-digit code.\n'
|
||||
printf 'Saved values go to macOS Keychain service openconnect-lite.\n\n'
|
||||
|
||||
if $password_present; then
|
||||
@@ -762,9 +900,9 @@ _configure_keychain() {
|
||||
_prompt_read password "Corporate LDAP password for $KC_USERNAME: " 1
|
||||
fi
|
||||
if $totp_present; then
|
||||
_prompt_read totp_secret "TOTP seed BASE32 [leave empty to keep saved seed]: " 1
|
||||
_prompt_read totp_secret "2FA setup secret BASE32 [leave empty to keep saved secret]: " 1
|
||||
else
|
||||
_prompt_read totp_secret "TOTP seed BASE32 from 2FA setup: " 1
|
||||
_prompt_read totp_secret "2FA setup secret BASE32: " 1
|
||||
fi
|
||||
|
||||
if [[ -z "$password" && "$password_present" != "true" ]]; then
|
||||
@@ -772,7 +910,7 @@ _configure_keychain() {
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$totp_secret" && "$totp_present" != "true" ]]; then
|
||||
printf 'TOTP seed is required because no saved seed was found. Use the BASE32 setup secret, not the current 6-digit code.\n' >&2
|
||||
printf '2FA setup secret is required because no saved secret was found. Use the BASE32 setup secret, not the current 6-digit code.\n' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -784,6 +922,53 @@ _configure_keychain() {
|
||||
printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||
}
|
||||
|
||||
_configure_login() {
|
||||
local password password_present=false
|
||||
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||
|
||||
printf 'Manual LDAP login setup for Lemana VPN\n'
|
||||
printf 'User: %s\n' "$KC_USERNAME"
|
||||
printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n'
|
||||
printf 'Saved values go to macOS Keychain service openconnect-lite.\n'
|
||||
printf 'One-time 2FA codes will still be entered manually during VPN login.\n\n'
|
||||
|
||||
if $password_present; then
|
||||
_prompt_read password "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " 1
|
||||
else
|
||||
_prompt_read password "Corporate LDAP password for $KC_USERNAME: " 1
|
||||
fi
|
||||
|
||||
if [[ -z "$password" && "$password_present" != "true" ]]; then
|
||||
printf 'LDAP password is required because no saved password was found.\n' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
_store_keychain "$password" ""
|
||||
printf 'Login and password are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||
}
|
||||
|
||||
_ensure_keychain_login() {
|
||||
local password_present=false
|
||||
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||
|
||||
if [[ "$password_present" == "true" ]]; then
|
||||
_emit '{"event":"keychain_ready","source":"keychain","scope":"login"}' \
|
||||
"Keychain login is ready: saved LDAP password is available for $KC_USERNAME."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_emit '{"event":"keychain_required","source":"keychain","scope":"login"}' \
|
||||
"Saved LDAP password is missing."
|
||||
|
||||
if ! _can_prompt; then
|
||||
_emit '{"event":"error","message":"Saved LDAP password is missing. Run vpn --configure-login in Terminal, or connect fully manually."}' \
|
||||
"Saved LDAP password is missing. Run: vpn --configure-login"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_configure_login
|
||||
}
|
||||
|
||||
_ensure_keychain_credentials() {
|
||||
local password_present=false totp_present=false
|
||||
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||
@@ -793,7 +978,7 @@ _ensure_keychain_credentials() {
|
||||
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
|
||||
else
|
||||
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and TOTP seed are available for $KC_USERNAME."
|
||||
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and 2FA setup secret are available for $KC_USERNAME."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
@@ -805,8 +990,8 @@ _ensure_keychain_credentials() {
|
||||
fi
|
||||
|
||||
if ! _can_prompt; then
|
||||
_emit '{"event":"error","message":"LDAP credentials are missing. Run vpn --configure-keychain in Terminal, or reinstall with --configure-keychain."}' \
|
||||
"LDAP credentials are missing. Run: vpn --configure-keychain"
|
||||
_emit '{"event":"error","message":"Automatic sign-in is not configured. Run vpn --configure-keychain in Terminal, or use vpn --manual-full."}' \
|
||||
"Automatic sign-in is not configured. Run: vpn --configure-keychain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -926,6 +1111,9 @@ except Exception:
|
||||
|
||||
_sync_credentials() {
|
||||
case "$CREDENTIAL_SOURCE" in
|
||||
none)
|
||||
_emit '{"event":"credential_source","source":"none"}' "Credential source: none"
|
||||
;;
|
||||
bitwarden)
|
||||
_sync_bitwarden
|
||||
;;
|
||||
@@ -1010,6 +1198,12 @@ if $STATUS_MODE; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if $CONFIGURE_LOGIN_MODE; then
|
||||
_ensure_username
|
||||
_configure_login
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if $CONFIGURE_KEYCHAIN_MODE; then
|
||||
_ensure_username
|
||||
_configure_keychain
|
||||
@@ -1027,38 +1221,64 @@ else
|
||||
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
|
||||
fi
|
||||
|
||||
if [[ "$CONNECT_MODE" == "manual-full" ]]; then
|
||||
_emit '{"event":"credential_source","source":"manual-full","message":"Manual-full mode skips saved credential checks"}' \
|
||||
"Manual-full mode: skipping saved credential checks."
|
||||
else
|
||||
_ensure_username
|
||||
_sync_credentials
|
||||
_ensure_keychain_credentials
|
||||
fi
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
_emit '{"event":"auth_profile","profile":"manual","message":"Manual mode skips saved credential checks"}' \
|
||||
"Manual mode: open the visible browser and enter login, password, and one-time code manually."
|
||||
;;
|
||||
saved-login)
|
||||
_ensure_username
|
||||
_sync_credentials
|
||||
_ensure_keychain_login
|
||||
;;
|
||||
saved-totp|auto)
|
||||
_ensure_username
|
||||
_sync_credentials
|
||||
_ensure_keychain_credentials
|
||||
;;
|
||||
esac
|
||||
_patch_oc
|
||||
|
||||
_emit '{"event":"connecting"}' "Connecting to VPN (lemanapro)..."
|
||||
_write_status "{\"pid\":$$,\"state\":\"connecting\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||
_prepare_connection_log
|
||||
_emit '{"event":"log","message":"openconnect-lite log is enabled"}' "openconnect-lite log: $CONNECT_LOG_FILE"
|
||||
trap '_stop_connect_progress; _dns_cleanup; _clear_status' EXIT
|
||||
trap '_stop_connect_progress; _dns_cleanup; _cleanup_manual_openconnect_config; _clear_status' EXIT
|
||||
|
||||
display_mode="hidden"
|
||||
display_mode="shown"
|
||||
log_level=""
|
||||
autofill_debug="${LEMANA_VPN_AUTOFILL_DEBUG:-0}"
|
||||
autofill_disable="${LEMANA_VPN_AUTOFILL_DISABLE:-0}"
|
||||
autofill_click="${LEMANA_VPN_AUTOFILL_CLICK:-1}"
|
||||
if [[ "$CONNECT_MODE" == "manual" ]]; then
|
||||
display_mode="shown"
|
||||
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."
|
||||
elif [[ "$CONNECT_MODE" == "manual-full" ]]; then
|
||||
display_mode="shown"
|
||||
autofill_disable="1"
|
||||
autofill_click="0"
|
||||
_emit '{"event":"manual_sso","autofill":false,"submit":false}' "Full manual mode: browser is visible, auto-fill is disabled, submit is not pressed."
|
||||
fi
|
||||
autofill_disable="${LEMANA_VPN_AUTOFILL_DISABLE:-1}"
|
||||
autofill_click="${LEMANA_VPN_AUTOFILL_CLICK:-0}"
|
||||
autofill_totp="${LEMANA_VPN_AUTOFILL_TOTP:-0}"
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
autofill_disable="1"
|
||||
autofill_click="0"
|
||||
autofill_totp="0"
|
||||
_prepare_manual_openconnect_config
|
||||
_emit '{"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false}' "Manual mode: browser is visible, auto-fill is disabled, submit is not pressed."
|
||||
;;
|
||||
saved-login)
|
||||
autofill_disable="0"
|
||||
autofill_click="0"
|
||||
autofill_totp="0"
|
||||
_emit '{"event":"manual_sso","auth_profile":"saved-login","autofill":true,"totp":false,"submit":false}' "Saved-login mode: browser is visible, login/password are filled, one-time code is manual."
|
||||
;;
|
||||
saved-totp)
|
||||
autofill_disable="0"
|
||||
autofill_click="0"
|
||||
autofill_totp="1"
|
||||
_emit '{"event":"manual_sso","auth_profile":"saved-totp","autofill":true,"totp":true,"submit":false}' "Saved-TOTP mode: browser is visible, saved fields are filled, submit is not pressed."
|
||||
;;
|
||||
auto)
|
||||
display_mode="hidden"
|
||||
autofill_disable="0"
|
||||
autofill_click="1"
|
||||
autofill_totp="1"
|
||||
_emit '{"event":"auto_sso","auth_profile":"auto","autofill":true,"submit":true}' "Auto mode: hidden browser, auto-fill and auto-submit."
|
||||
;;
|
||||
esac
|
||||
if $DEBUG; then
|
||||
display_mode="shown"
|
||||
log_level="--log-level debug"
|
||||
@@ -1069,9 +1289,11 @@ reconnect_count=0
|
||||
while true; do
|
||||
_start_connect_progress
|
||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" \
|
||||
XDG_CONFIG_HOME="$OC_RUNTIME_XDG_CONFIG_HOME" \
|
||||
LEMANA_VPN_AUTOFILL_DEBUG="$autofill_debug" \
|
||||
LEMANA_VPN_AUTOFILL_DISABLE="$autofill_disable" \
|
||||
LEMANA_VPN_AUTOFILL_CLICK="$autofill_click" \
|
||||
LEMANA_VPN_AUTOFILL_TOTP="$autofill_totp" \
|
||||
"$OC_BIN" --browser-display-mode "$display_mode" $log_level 2>&1 \
|
||||
| _filter_output
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
|
||||
318
install.sh
318
install.sh
@@ -11,25 +11,29 @@ OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||
USERNAME="${LEMANA_VPN_USERNAME:-}"
|
||||
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
|
||||
USE_BITWARDEN=1
|
||||
USE_TOUCHID=1
|
||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-none}"
|
||||
AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE:-manual}"
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
INSTALL_SUDOERS=1
|
||||
INSTALL_ALIASES=1
|
||||
INSTALL_APP=1
|
||||
INSTALL_AUTOSTART=1
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
DRY_RUN=0
|
||||
FORCE=0
|
||||
INTERACTIVE=auto
|
||||
BITWARDEN_FORCED=0
|
||||
CREDENTIAL_SOURCE_FORCED=0
|
||||
AUTH_PROFILE_FORCED=0
|
||||
TOUCHID_FORCED=0
|
||||
SUDOERS_FORCED=0
|
||||
SHELL_FORCED=0
|
||||
APP_FORCED=0
|
||||
AUTOSTART_FORCED=0
|
||||
CONFIGURE_KEYCHAIN_FORCED=0
|
||||
CONFIGURE_LOGIN_FORCED=0
|
||||
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
||||
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
|
||||
|
||||
@@ -75,12 +79,14 @@ Usage:
|
||||
sh install.sh [options]
|
||||
|
||||
Options:
|
||||
--with-bitwarden Install/use Bitwarden CLI module (default)
|
||||
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
||||
--credential-source VALUE Credential source: bitwarden or keychain
|
||||
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
||||
--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
|
||||
--with-touchid Install/use keychain-fingerprint Touch ID helper with Bitwarden
|
||||
--without-touchid Do not install/use Touch ID helper
|
||||
--configure-keychain Prompt for LDAP password and TOTP secret after install
|
||||
--configure-login Prompt for LDAP username/password after install
|
||||
--configure-keychain Prompt for LDAP password and permanent 2FA setup code after install
|
||||
--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
|
||||
@@ -92,15 +98,15 @@ Options:
|
||||
--without-autostart Do not install LaunchAgent
|
||||
--interactive Ask before installing optional missing modules
|
||||
--non-interactive Use selected/default modules without prompts
|
||||
--minimal Same as --without-bitwarden --without-touchid
|
||||
--minimal Manual-first install without optional credential helpers
|
||||
--dry-run Print actions without changing files
|
||||
--force Reinstall files even when present
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
sh install.sh
|
||||
sh install.sh --minimal --configure-keychain
|
||||
sh install.sh --credential-source keychain --configure-keychain
|
||||
sh install.sh --configure-login
|
||||
sh install.sh --credential-source keychain --auth-profile saved-totp --configure-keychain
|
||||
sh install.sh --without-touchid
|
||||
USAGE
|
||||
}
|
||||
@@ -112,29 +118,53 @@ while [ "$#" -gt 0 ]; do
|
||||
USE_BITWARDEN=1
|
||||
BITWARDEN_FORCED=1
|
||||
CREDENTIAL_SOURCE_FORCED=1
|
||||
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "manual" ]; then
|
||||
AUTH_PROFILE="saved-totp"
|
||||
fi
|
||||
;;
|
||||
--without-bitwarden)
|
||||
CREDENTIAL_SOURCE="keychain"
|
||||
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ]; then
|
||||
CREDENTIAL_SOURCE="none"
|
||||
fi
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
BITWARDEN_FORCED=1
|
||||
CREDENTIAL_SOURCE_FORCED=1
|
||||
;;
|
||||
--credential-source)
|
||||
shift
|
||||
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
|
||||
[ "$#" -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"
|
||||
fi
|
||||
;;
|
||||
keychain)
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
;;
|
||||
*)
|
||||
echo "--credential-source requires bitwarden or keychain" >&2
|
||||
echo "--credential-source requires none, keychain, or bitwarden" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--auth-profile)
|
||||
shift
|
||||
[ "$#" -gt 0 ] || { echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2; exit 1; }
|
||||
AUTH_PROFILE="$1"
|
||||
AUTH_PROFILE_FORCED=1
|
||||
case "$AUTH_PROFILE" in
|
||||
manual|saved-login|saved-totp|auto) ;;
|
||||
*)
|
||||
echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -147,9 +177,31 @@ while [ "$#" -gt 0 ]; do
|
||||
USE_TOUCHID=0
|
||||
TOUCHID_FORCED=1
|
||||
;;
|
||||
--configure-login)
|
||||
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
|
||||
fi
|
||||
;;
|
||||
--configure-keychain)
|
||||
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
|
||||
@@ -195,11 +247,13 @@ while [ "$#" -gt 0 ]; do
|
||||
--interactive) INTERACTIVE=1 ;;
|
||||
--non-interactive) INTERACTIVE=0 ;;
|
||||
--minimal)
|
||||
CREDENTIAL_SOURCE="keychain"
|
||||
CREDENTIAL_SOURCE="none"
|
||||
AUTH_PROFILE="manual"
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
BITWARDEN_FORCED=1
|
||||
CREDENTIAL_SOURCE_FORCED=1
|
||||
AUTH_PROFILE_FORCED=1
|
||||
TOUCHID_FORCED=1
|
||||
;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
@@ -329,54 +383,121 @@ yes_no() {
|
||||
done
|
||||
}
|
||||
|
||||
choose_credential_source_interactive() {
|
||||
log_step "Настраиваю способ хранения credentials"
|
||||
log_detail "Можно выбрать Bitwarden или бесплатный macOS Keychain flow с ручным вводом LDAP password и TOTP seed."
|
||||
print_install_intro() {
|
||||
log_step "Lemana VPN установит приложение для подключения к корпоративному VPN"
|
||||
log_detail "По умолчанию ничего не сохраняется: при подключении откроется окно входа, а логин, пароль и одноразовый код вводятся вручную."
|
||||
log_detail "Дополнительные удобства можно включить сейчас или позже."
|
||||
}
|
||||
|
||||
choose_advanced_signin_interactive() {
|
||||
while :; do
|
||||
prompt_printf '\n'
|
||||
prompt_printf '%sКак хранить VPN credentials?%s\n' "$C_BOLD" "$C_RESET"
|
||||
prompt_printf ' 1) Bitwarden -> macOS Keychain\n'
|
||||
prompt_printf ' 2) macOS Keychain: ввести LDAP password и TOTP seed сейчас\n'
|
||||
prompt_printf ' 3) macOS Keychain: настрою вручную позже\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)
|
||||
CREDENTIAL_SOURCE=bitwarden
|
||||
USE_BITWARDEN=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)
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
AUTH_PROFILE=saved-totp
|
||||
CREDENTIAL_SOURCE=bitwarden
|
||||
USE_BITWARDEN=1
|
||||
CONFIGURE_LOGIN=0
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
;;
|
||||
*)
|
||||
prompt_printf 'Введите 1, 2 или 3.\n'
|
||||
prompt_printf 'Введите 1, 2 или 3. Если не уверены, нажмите Enter.\n'
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
choose_auth_profile_interactive() {
|
||||
log_step "Выбираю режим подключения"
|
||||
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_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
|
||||
CONFIGURE_LOGIN=1
|
||||
CONFIGURE_KEYCHAIN=0
|
||||
;;
|
||||
3)
|
||||
choose_advanced_signin_interactive
|
||||
;;
|
||||
*)
|
||||
prompt_printf 'Введите 1, 2 или 3. Если не уверены, нажмите Enter.\n'
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
break
|
||||
done
|
||||
|
||||
case "$CREDENTIAL_SOURCE" in
|
||||
bitwarden)
|
||||
log_info "Credential source: Bitwarden sync into macOS Keychain"
|
||||
case "$AUTH_PROFILE" in
|
||||
manual)
|
||||
log_info "Режим подключения: ручной ввод логина, пароля и одноразового кода"
|
||||
;;
|
||||
keychain)
|
||||
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||
log_info "Credential source: macOS Keychain, credentials будут запрошены после установки"
|
||||
else
|
||||
log_info "Credential source: macOS Keychain, credentials можно настроить позже через vpn --configure-keychain"
|
||||
fi
|
||||
saved-login)
|
||||
log_info "Режим подключения: логин и пароль будут сохранены в macOS Keychain, одноразовый код вводится вручную"
|
||||
;;
|
||||
saved-totp)
|
||||
log_info "Режим подключения: сохранённые данные будут подставляться в форму входа"
|
||||
;;
|
||||
auto)
|
||||
log_info "Режим подключения: автоматический вход включается только через явный auto-запуск"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -413,13 +534,31 @@ print_detected_state() {
|
||||
log " Menu Bar app: $(bool_word test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
||||
log " LaunchAgent: $(bool_word test -f "$LAUNCH_AGENT")"
|
||||
log " Keychain password: $(bool_word keychain_has openconnect-lite "$USERNAME")"
|
||||
log " Keychain TOTP seed: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
|
||||
log " Keychain 2FA setup: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
|
||||
}
|
||||
|
||||
choose_modules() {
|
||||
if interactive_enabled; then
|
||||
print_install_intro
|
||||
fi
|
||||
print_detected_state
|
||||
|
||||
case "$AUTH_PROFILE" in
|
||||
manual|saved-login|saved-totp|auto) ;;
|
||||
*)
|
||||
die "Unknown auth profile: $AUTH_PROFILE"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "none" ] && [ "$AUTH_PROFILE" != "manual" ]; then
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
fi
|
||||
|
||||
case "$CREDENTIAL_SOURCE" in
|
||||
none)
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
;;
|
||||
bitwarden)
|
||||
USE_BITWARDEN=1
|
||||
;;
|
||||
@@ -438,18 +577,19 @@ choose_modules() {
|
||||
fi
|
||||
|
||||
log_info "Interactive prompts: on"
|
||||
log_detail "Установщик проведёт по выбору credential source и отсутствующих опциональных модулей; флаги командной строки имеют приоритет."
|
||||
log_detail "Установщик проведёт по понятному выбору режима входа; флаги командной строки имеют приоритет."
|
||||
|
||||
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||
choose_credential_source_interactive
|
||||
if [ "$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 CLI не найден. Поставить модуль Bitwarden?" y; then
|
||||
if yes_no "Bitwarden не найден на этом Mac. Установить его сейчас?" n; then
|
||||
CREDENTIAL_SOURCE=bitwarden
|
||||
USE_BITWARDEN=1
|
||||
else
|
||||
CREDENTIAL_SOURCE=keychain
|
||||
AUTH_PROFILE=manual
|
||||
CREDENTIAL_SOURCE=none
|
||||
USE_BITWARDEN=0
|
||||
USE_TOUCHID=0
|
||||
fi
|
||||
@@ -458,7 +598,7 @@ choose_modules() {
|
||||
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 helper не найден. Собрать и установить?" y; then
|
||||
if yes_no "Разрешить Touch ID для открытия Bitwarden?" n; then
|
||||
USE_TOUCHID=1
|
||||
else
|
||||
USE_TOUCHID=0
|
||||
@@ -504,9 +644,17 @@ choose_modules() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
||||
if [ "$CONFIGURE_LOGIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-login" ] && [ "$CONFIGURE_LOGIN" -eq 0 ]; then
|
||||
if ! keychain_has openconnect-lite "$USERNAME"; then
|
||||
if yes_no "Запомнить логин и пароль в macOS Keychain после установки?" y; then
|
||||
CONFIGURE_LOGIN=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-totp" ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
||||
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
|
||||
if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then
|
||||
if yes_no "Сохранить пароль и постоянный код настройки 2FA в macOS Keychain после установки?" y; then
|
||||
CONFIGURE_KEYCHAIN=1
|
||||
fi
|
||||
fi
|
||||
@@ -516,7 +664,13 @@ choose_modules() {
|
||||
script_dir() {
|
||||
case "$0" in
|
||||
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
|
||||
*) return 1 ;;
|
||||
*)
|
||||
if [ -f "$PWD/$0" ]; then
|
||||
pwd
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -569,7 +723,7 @@ install_homebrew_packages() {
|
||||
log_ok "Homebrew package already installed: bitwarden-cli"
|
||||
else
|
||||
log_info "Installing Homebrew package: bitwarden-cli"
|
||||
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и TOTP seed из item '$BW_ITEM'."
|
||||
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и постоянный код настройки 2FA из item '$BW_ITEM'."
|
||||
run brew install bitwarden-cli
|
||||
fi
|
||||
else
|
||||
@@ -615,7 +769,7 @@ install_cli() {
|
||||
install_config() {
|
||||
tmp="$1"
|
||||
log_step "Записываю конфигурацию"
|
||||
log_detail "Здесь сохраняются выбранные модули, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
|
||||
log_detail "Здесь сохраняются выбранный режим входа, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
|
||||
|
||||
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
|
||||
|
||||
@@ -628,6 +782,7 @@ install_config() {
|
||||
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\"
|
||||
@@ -878,7 +1033,7 @@ EOF
|
||||
|
||||
maybe_login_bitwarden() {
|
||||
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
|
||||
log_skip "Credential source is keychain; пропускаю Bitwarden login."
|
||||
log_skip "Bitwarden не выбран; пропускаю Bitwarden login."
|
||||
return 0
|
||||
fi
|
||||
if ! command -v bw >/dev/null 2>&1; then
|
||||
@@ -899,6 +1054,43 @@ maybe_login_bitwarden() {
|
||||
fi
|
||||
}
|
||||
|
||||
auth_profile_label() {
|
||||
case "$AUTH_PROFILE" in
|
||||
manual) printf 'ручной ввод без сохранения паролей' ;;
|
||||
saved-login) printf 'сохранить логин и пароль, одноразовый код вводить вручную' ;;
|
||||
saved-totp) printf 'подставлять сохранённые данные в окно входа' ;;
|
||||
auto) printf 'автоматический вход только при явном запуске' ;;
|
||||
*) printf '%s' "$AUTH_PROFILE" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
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
|
||||
log_info "Status: vpn --status"
|
||||
if [ "$INSTALL_APP" -eq 1 ]; then
|
||||
log_info "App: open '$APP_DIR'"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
|
||||
|
||||
@@ -909,7 +1101,12 @@ main() {
|
||||
|
||||
log_step "Начинаю установку Lemana VPN"
|
||||
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
|
||||
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||
if interactive_enabled; then
|
||||
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"
|
||||
fi
|
||||
|
||||
install_homebrew_packages
|
||||
install_openconnect_lite
|
||||
@@ -924,20 +1121,17 @@ main() {
|
||||
install_shell_aliases "$tmp"
|
||||
maybe_login_bitwarden
|
||||
|
||||
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||
if [ "$CONFIGURE_LOGIN" -eq 1 ]; then
|
||||
log_step "Записываю логин и пароль в macOS Keychain"
|
||||
log_detail "Будет запрошен корпоративный LDAP-пароль. Одноразовый код при подключении останется ручным."
|
||||
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-login
|
||||
elif [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||
log_step "Записываю credentials в macOS Keychain"
|
||||
log_detail "Будут запрошены LDAP-пароль и постоянный TOTP seed, не текущий 30-секундный код."
|
||||
log_detail "Будут запрошены LDAP-пароль и постоянный код настройки 2FA, не текущий 30-секундный код."
|
||||
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
|
||||
fi
|
||||
|
||||
log ""
|
||||
log_ok "Done."
|
||||
log_info "Open a new shell or run: exec zsh"
|
||||
log_info "Connect: vpn"
|
||||
log_info "Status: vpn --status"
|
||||
if [ "$INSTALL_APP" -eq 1 ]; then
|
||||
log_info "App: open '$APP_DIR'"
|
||||
fi
|
||||
print_final_next_steps
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
349
tests/smoke.sh
349
tests/smoke.sh
@@ -15,13 +15,18 @@ 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: credential_source=keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
||||
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 'Проверяю 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 '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 '
|
||||
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
|
||||
|
||||
esc="$(printf '\033')"
|
||||
if printf '%s\n' "$output" | grep -q "$esc"; then
|
||||
@@ -33,34 +38,130 @@ if command -v expect >/dev/null 2>&1; then
|
||||
interactive_output="$(
|
||||
ROOT="$ROOT" expect <<'EXPECT'
|
||||
set timeout 30
|
||||
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
|
||||
expect "Как хранить VPN credentials?"
|
||||
expect "Выбор"
|
||||
send "2\r"
|
||||
expect eof
|
||||
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
|
||||
expect "Как подключаться по умолчанию?"
|
||||
expect "Выбор"
|
||||
send "\r"
|
||||
expect "Сохранить логин и пароль"
|
||||
send "y\r"
|
||||
expect eof
|
||||
EXPECT
|
||||
)"
|
||||
|
||||
printf '%s\n' "$interactive_output" | grep -q 'Как хранить VPN credentials?'
|
||||
printf '%s\n' "$interactive_output" | grep -q '1) Bitwarden'
|
||||
printf '%s\n' "$interactive_output" | grep -q '2) macOS Keychain: ввести LDAP password и TOTP seed сейчас'
|
||||
printf '%s\n' "$interactive_output" | grep -q '3) macOS Keychain: настрою вручную позже'
|
||||
printf '%s\n' "$interactive_output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=0 shell=0 app=0 autostart=0'
|
||||
printf '%s\n' "$interactive_output" | grep -q "vpn-lemanapro.sh --configure-keychain"
|
||||
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 '+ .*/vpn-lemanapro.sh --configure-login'
|
||||
if printf '%s\n' "$interactive_output" | grep -Eq 'credential_source|auth_profile|TOTP seed'; then
|
||||
echo "basic interactive wizard should not expose technical credential wording" >&2
|
||||
exit 1
|
||||
fi
|
||||
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}'
|
||||
|
||||
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","keychain_ready":false}'
|
||||
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 '"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","keychain_ready":false}'
|
||||
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 '"bitwarden":{"enabled":true'
|
||||
|
||||
status_password_bin="$TMP_DIR/status-password-bin"
|
||||
mkdir -p "$status_password_bin"
|
||||
cat > "$status_password_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
|
||||
""|totp/*) exit 1 ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$status_password_bin/security"
|
||||
|
||||
status_full_bin="$TMP_DIR/status-full-bin"
|
||||
mkdir -p "$status_full_bin"
|
||||
cat > "$status_full_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
|
||||
[ -n "$account" ]
|
||||
exit $?
|
||||
fi
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$status_full_bin/security"
|
||||
|
||||
saved_login_missing_status="$(
|
||||
LEMANA_VPN_USERNAME="lemana-status-login-missing" \
|
||||
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE=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"}'
|
||||
|
||||
saved_login_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 \
|
||||
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}'
|
||||
|
||||
saved_totp_missing_status="$(
|
||||
LEMANA_VPN_USERNAME="lemana-status-totp-missing" \
|
||||
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"}'
|
||||
|
||||
saved_totp_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 \
|
||||
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}'
|
||||
|
||||
auto_ready_status="$(
|
||||
PATH="$status_full_bin:$PATH" \
|
||||
LEMANA_VPN_USERNAME="lemana-status-auto-ready" \
|
||||
LEMANA_VPN_AUTH_PROFILE=auto \
|
||||
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}'
|
||||
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
|
||||
@@ -75,13 +176,32 @@ grep -q -- '--manual-full' "$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 '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 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||
grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||
grep -q 'connect(mode: .manualFull)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||
grep -q 'Подключить полностью вручную' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||
grep -q 'ForEach(vpnManager.availableLaunchModes)' "$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 'vpnManager.configureLogin()' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.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
|
||||
fi
|
||||
if grep -Fq 'Button("Подставить только логин и пароль")' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"; then
|
||||
echo "disconnected app menu should not hardcode saved-login button visibility" >&2
|
||||
exit 1
|
||||
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 "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"
|
||||
grep -q -- '--configure-login' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||
@@ -178,6 +298,8 @@ grep -q 'script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)' "$f
|
||||
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"
|
||||
grep -q 'LEMANA_VPN_AUTOFILL_TOTP' "$fake_webengine"
|
||||
grep -q 'rule.fill == "totp"' "$fake_webengine"
|
||||
if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then
|
||||
echo "patched auto-fill should keep the original ApplicationWorld behavior" >&2
|
||||
exit 1
|
||||
@@ -230,6 +352,7 @@ set +e
|
||||
manual_output="$(
|
||||
HOME="$HOME" \
|
||||
LEMANA_VPN_USERNAME="$missing_user" \
|
||||
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||
)"
|
||||
@@ -238,9 +361,9 @@ set -e
|
||||
|
||||
[ "$manual_code" -ne 0 ]
|
||||
printf '%s\n' "$manual_output" | grep -q '"event":"keychain_required"'
|
||||
printf '%s\n' "$manual_output" | grep -q 'vpn --configure-keychain'
|
||||
printf '%s\n' "$manual_output" | grep -q 'vpn --configure-login'
|
||||
if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then
|
||||
echo "missing manual credentials should fail before VPN cleanup trap is installed" >&2
|
||||
echo "missing saved login should fail before VPN cleanup trap is installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -249,10 +372,11 @@ mkdir -p "$missing_username_home"
|
||||
set +e
|
||||
missing_username_output="$(
|
||||
HOME="$missing_username_home" \
|
||||
LEMANA_VPN_CONFIG_DIR="$missing_username_home/.config/lemana-vpn" \
|
||||
OPENCONNECT_LITE_CONFIG_DIR="$missing_username_home/.config/openconnect-lite" \
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||
LEMANA_VPN_CONFIG_DIR="$missing_username_home/.config/lemana-vpn" \
|
||||
OPENCONNECT_LITE_CONFIG_DIR="$missing_username_home/.config/openconnect-lite" \
|
||||
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||
)"
|
||||
missing_username_code=$?
|
||||
set -e
|
||||
@@ -294,6 +418,25 @@ cat > "$fake_oc_bin" <<'SH'
|
||||
printf 'args=%s\n' "$*"
|
||||
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:-}"
|
||||
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
config_file="$config_home/openconnect-lite/config.toml"
|
||||
printf 'xdg_config_home=%s\n' "$config_home"
|
||||
if [ -f "$config_file" ]; then
|
||||
if grep -q '^\[credentials\]' "$config_file"; then
|
||||
printf 'credentials_block=1\n'
|
||||
else
|
||||
printf 'credentials_block=0\n'
|
||||
fi
|
||||
if grep -q 'username = ""' "$config_file"; then
|
||||
printf 'empty_username=1\n'
|
||||
else
|
||||
printf 'empty_username=0\n'
|
||||
fi
|
||||
else
|
||||
printf 'credentials_block=missing\n'
|
||||
printf 'empty_username=missing\n'
|
||||
fi
|
||||
} > "$LEMANA_VPN_CAPTURE_LAUNCH"
|
||||
exit 0
|
||||
SH
|
||||
@@ -307,9 +450,139 @@ 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'
|
||||
on_disconnect = "true"
|
||||
|
||||
[default_profile]
|
||||
address = "vpn.lemanapro.ru"
|
||||
user_group = ""
|
||||
name = "Default-K"
|
||||
|
||||
[credentials]
|
||||
username = ""
|
||||
TOML
|
||||
default_manual_output="$(
|
||||
HOME="$default_manual_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_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
|
||||
)"
|
||||
|
||||
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
|
||||
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"
|
||||
cat > "$fake_login_keychain_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
|
||||
""|totp/*) exit 1 ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$fake_login_keychain_bin/security"
|
||||
|
||||
saved_login_capture="$TMP_DIR/saved-login-capture"
|
||||
saved_login_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_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" \
|
||||
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
|
||||
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"
|
||||
|
||||
saved_totp_capture="$TMP_DIR/saved-totp-capture"
|
||||
saved_totp_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_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" \
|
||||
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"
|
||||
|
||||
manual_full_no_credentials_home="$TMP_DIR/manual-full-no-credentials-home"
|
||||
manual_full_no_credentials_capture="$TMP_DIR/manual-full-no-credentials-capture"
|
||||
mkdir -p "$manual_full_no_credentials_home"
|
||||
mkdir -p "$manual_full_no_credentials_home/.config/openconnect-lite"
|
||||
cat > "$manual_full_no_credentials_home/.config/openconnect-lite/config.toml" <<'TOML'
|
||||
on_disconnect = "true"
|
||||
|
||||
[default_profile]
|
||||
address = "vpn.lemanapro.ru"
|
||||
user_group = ""
|
||||
name = "Default-K"
|
||||
|
||||
[credentials]
|
||||
username = ""
|
||||
TOML
|
||||
manual_full_no_credentials_output="$(
|
||||
HOME="$manual_full_no_credentials_home" \
|
||||
PATH="$fake_missing_keychain_bin:$PATH" \
|
||||
@@ -327,7 +600,7 @@ manual_full_no_credentials_output="$(
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
||||
)"
|
||||
|
||||
printf '%s\n' "$manual_full_no_credentials_output" | grep -q '"event":"manual_sso","autofill":false,"submit":false'
|
||||
printf '%s\n' "$manual_full_no_credentials_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":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
|
||||
@@ -335,6 +608,9 @@ fi
|
||||
grep -q -- '--browser-display-mode shown' "$manual_full_no_credentials_capture"
|
||||
grep -q '^autofill_disable=1$' "$manual_full_no_credentials_capture"
|
||||
grep -q '^autofill_click=0$' "$manual_full_no_credentials_capture"
|
||||
grep -q '^autofill_totp=0$' "$manual_full_no_credentials_capture"
|
||||
grep -q '^credentials_block=0$' "$manual_full_no_credentials_capture"
|
||||
grep -q '^empty_username=0$' "$manual_full_no_credentials_capture"
|
||||
|
||||
if command -v expect >/dev/null 2>&1; then
|
||||
first_username_home="$TMP_DIR/first-username-home"
|
||||
@@ -393,17 +669,18 @@ manual_full_output="$(
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
||||
)"
|
||||
|
||||
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","autofill":false,"submit":false'
|
||||
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":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"
|
||||
grep -q '^autofill_totp=0$' "$manual_full_capture"
|
||||
|
||||
fake_oc_python="$TMP_DIR/fake-oc-python"
|
||||
captured_totp="$TMP_DIR/captured-totp"
|
||||
cat > "$fake_oc_python" <<'SH'
|
||||
#!/bin/sh
|
||||
cat >/dev/null
|
||||
printf '%s\n' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
||||
printf '%s' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
||||
SH
|
||||
chmod +x "$fake_oc_python"
|
||||
|
||||
@@ -420,6 +697,24 @@ configure_output="$(
|
||||
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
||||
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
||||
|
||||
captured_login_totp="$TMP_DIR/captured-login-totp"
|
||||
configure_login_output="$(
|
||||
printf 'ldap-password\n' |
|
||||
HOME="$HOME" \
|
||||
LEMANA_VPN_USERNAME="lemana-login-$$" \
|
||||
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
|
||||
LEMANA_VPN_CAPTURE_TOTP="$captured_login_totp" \
|
||||
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-login
|
||||
)"
|
||||
|
||||
printf '%s\n' "$configure_login_output" | grep -q 'Login and password are ready in macOS Keychain'
|
||||
if [ -s "$captured_login_totp" ]; then
|
||||
echo "configure-login must not store a TOTP seed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fake_pwd="$TMP_DIR/fake-pwd"
|
||||
mkdir -p "$fake_pwd/bin"
|
||||
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
||||
|
||||
Reference in New Issue
Block a user