324 lines
13 KiB
Bash
Executable File
324 lines
13 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
TMP_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
|
|
|
|
export HOME="$TMP_DIR/home"
|
|
export LEMANA_VPN_BIN_DIR="$HOME/bin"
|
|
export LEMANA_VPN_CONFIG_DIR="$HOME/.config/lemana-vpn"
|
|
export OPENCONNECT_LITE_CONFIG_DIR="$HOME/.config/openconnect-lite"
|
|
mkdir -p "$HOME"
|
|
|
|
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 'Проверяю 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'
|
|
|
|
esc="$(printf '\033')"
|
|
if printf '%s\n' "$output" | grep -q "$esc"; then
|
|
echo "non-tty dry-run output contains ANSI color codes" >&2
|
|
exit 1
|
|
fi
|
|
|
|
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
|
|
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"
|
|
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":'
|
|
|
|
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 '"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 '"bitwarden":{"enabled":true'
|
|
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
|
|
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
|
|
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
|
|
grep -q -- '--auto' "$ROOT/bin/vpn-lemanapro.sh"
|
|
grep -q -- '--manual' "$ROOT/bin/vpn-lemanapro.sh"
|
|
grep -q -- '--manual-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 '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 '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"
|
|
|
|
fake_webengine="$TMP_DIR/webengine_process.py"
|
|
fake_authenticator="$TMP_DIR/authenticator.py"
|
|
cat > "$fake_webengine" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
class Browser:
|
|
def run(self, display_mode, credentials, url_pattern, rules):
|
|
argv = sys.argv.copy()
|
|
if display_mode == "hidden":
|
|
argv += ["-platform", "minimal"]
|
|
|
|
if credentials:
|
|
logger.info("Initiating autologin", cred=credentials)
|
|
for url_pattern, rules in auto_fill_rules.items():
|
|
script = QWebEngineScript()
|
|
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
|
script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)
|
|
script.setSourceCode(
|
|
f"""
|
|
// ==UserScript==
|
|
// @include {url_pattern}
|
|
// ==/UserScript==
|
|
|
|
function autoFill() {{
|
|
{get_selectors(rules, credentials)}
|
|
setTimeout(autoFill, 1000);
|
|
}}
|
|
autoFill();
|
|
"""
|
|
)
|
|
self.page().scripts().insert(script)
|
|
|
|
def get_selectors(rules, credentials):
|
|
statements = []
|
|
for rule in rules:
|
|
selector = json.dumps(rule.selector)
|
|
if rule.action == "stop":
|
|
statements.append(
|
|
f"""var elem = document.querySelector({selector}); if (elem) {{ return; }}"""
|
|
)
|
|
elif rule.fill:
|
|
value = json.dumps(getattr(credentials, rule.fill, None))
|
|
if value:
|
|
statements.append(
|
|
f"""var elem = document.querySelector({selector}); if (elem) {{ elem.dispatchEvent(new Event("focus")); elem.value = {value}; elem.dispatchEvent(new Event("blur")); }}"""
|
|
)
|
|
else:
|
|
logger.warning(
|
|
"Credential info not available",
|
|
type=rule.fill,
|
|
possibilities=dir(credentials),
|
|
)
|
|
elif rule.action == "click":
|
|
statements.append(
|
|
f"""var elem = document.querySelector({selector}); if (elem) {{ elem.dispatchEvent(new Event("focus")); elem.click(); }}"""
|
|
)
|
|
return "\n".join(statements)
|
|
PY
|
|
|
|
cat > "$fake_authenticator" <<'PY'
|
|
import requests
|
|
import structlog
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
class Authenticator:
|
|
def _detect_authentication_target_url(self):
|
|
# Follow possible redirects in a GET request
|
|
# Authentication will occur using a POST request on the final URL
|
|
response = requests.get(self.host.vpn_url)
|
|
response.raise_for_status()
|
|
self.host.address = response.url
|
|
logger.debug("Auth target url", url=self.host.vpn_url)
|
|
|
|
def _start_authentication(self):
|
|
pass
|
|
PY
|
|
|
|
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
|
|
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
|
|
LEMANA_VPN_OC_PYTHON=python3 \
|
|
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/patch-backups" \
|
|
bash "$ROOT/bin/vpn-lemanapro.sh" --patch-only >/dev/null
|
|
|
|
grep -q '"offscreen"' "$fake_webengine"
|
|
grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$fake_webengine"
|
|
grep -q 'new RegExp' "$fake_webengine"
|
|
grep -q 'script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)' "$fake_webengine"
|
|
grep -q 'new Event("input", {{bubbles: true}})' "$fake_webengine"
|
|
grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$fake_webengine"
|
|
grep -q 'os.environ.get("LEMANA_VPN_AUTOFILL_CLICK", "1") != "0"' "$fake_webengine"
|
|
if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then
|
|
echo "patched auto-fill should keep the original ApplicationWorld behavior" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q '__lemanaVpnClicked' "$fake_webengine"; then
|
|
echo "patched auto-fill should stay stateless like the original working setup" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'valueSetter' "$fake_webengine"; then
|
|
echo "patched auto-fill should use the original direct value assignment with input/change events" >&2
|
|
exit 1
|
|
fi
|
|
grep -q 'from urllib.parse import urljoin' "$fake_authenticator"
|
|
grep -q 'self.session.get(self.host.vpn_url, allow_redirects=False)' "$fake_authenticator"
|
|
grep -q 'response.headers.get("Location")' "$fake_authenticator"
|
|
if grep -q 'requests.get(self.host.vpn_url)' "$fake_authenticator"; then
|
|
echo "auth target detection must not follow redirects with bare requests.get" >&2
|
|
exit 1
|
|
fi
|
|
|
|
status_text="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status)"
|
|
printf '%s\n' "$status_text" | grep -q 'Modules:'
|
|
printf '%s\n' "$status_text" | grep -q 'core='
|
|
printf '%s\n' "$status_text" | grep -q 'app='
|
|
printf '%s\n' "$status_text" | grep -q 'autostart='
|
|
|
|
uninstall_home="$TMP_DIR/uninstall-home"
|
|
mkdir -p "$uninstall_home"
|
|
uninstall_output="$(
|
|
HOME="$uninstall_home" \
|
|
LEMANA_VPN_BIN_DIR="$uninstall_home/bin" \
|
|
LEMANA_VPN_CONFIG_DIR="$uninstall_home/.config/lemana-vpn" \
|
|
OPENCONNECT_LITE_CONFIG_DIR="$uninstall_home/.config/openconnect-lite" \
|
|
sh "$ROOT/uninstall.sh" --dry-run --remove-keychain --remove-touchid-helper --remove-openconnect-lite
|
|
)"
|
|
|
|
printf '%s\n' "$uninstall_output" | grep -q 'Начинаю удаление Lemana VPN'
|
|
printf '%s\n' "$uninstall_output" | grep -q 'Проверяю runtime-патчи openconnect-lite'
|
|
printf '%s\n' "$uninstall_output" | grep -q 'Удаляю sudoers и DNS cleanup wrapper'
|
|
printf '%s\n' "$uninstall_output" | grep -q 'killall LemanaVPN # if running'
|
|
printf '%s\n' "$uninstall_output" | grep -q 'Удаляю VPN-записи из macOS Keychain'
|
|
|
|
if printf '%s\n' "$uninstall_output" | grep -q "$esc"; then
|
|
echo "non-tty uninstall dry-run output contains ANSI color codes" >&2
|
|
exit 1
|
|
fi
|
|
|
|
missing_user="lemana-smoke-missing-$$"
|
|
set +e
|
|
manual_output="$(
|
|
HOME="$HOME" \
|
|
LEMANA_VPN_USERNAME="$missing_user" \
|
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
|
)"
|
|
manual_code=$?
|
|
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'
|
|
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
|
|
exit 1
|
|
fi
|
|
|
|
fake_path_bin="$TMP_DIR/fake-path-bin"
|
|
mkdir -p "$fake_path_bin"
|
|
cat > "$fake_path_bin/security" <<'SH'
|
|
#!/bin/sh
|
|
if [ "${1:-}" = "find-generic-password" ]; then
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
SH
|
|
chmod +x "$fake_path_bin/security"
|
|
|
|
fake_oc_bin="$TMP_DIR/fake-openconnect-lite"
|
|
manual_full_capture="$TMP_DIR/manual-full-capture"
|
|
cat > "$fake_oc_bin" <<'SH'
|
|
#!/bin/sh
|
|
{
|
|
printf 'args=%s\n' "$*"
|
|
printf 'autofill_disable=%s\n' "${LEMANA_VPN_AUTOFILL_DISABLE:-}"
|
|
printf 'autofill_click=%s\n' "${LEMANA_VPN_AUTOFILL_CLICK:-}"
|
|
} > "$LEMANA_VPN_CAPTURE_LAUNCH"
|
|
exit 0
|
|
SH
|
|
chmod +x "$fake_oc_bin"
|
|
|
|
manual_full_output="$(
|
|
HOME="$HOME" \
|
|
PATH="$fake_path_bin:$PATH" \
|
|
LEMANA_VPN_USERNAME="lemana-manual-full-$$" \
|
|
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/manual-full-patch-backups" \
|
|
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
|
|
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-full.log" \
|
|
LEMANA_VPN_CAPTURE_LAUNCH="$manual_full_capture" \
|
|
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
|
)"
|
|
|
|
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","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"
|
|
|
|
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"
|
|
SH
|
|
chmod +x "$fake_oc_python"
|
|
|
|
configure_output="$(
|
|
printf 'ldap-password\notpauth://totp/Lemana:test?secret=abcd2345efgh6723&issuer=Lemana\n' |
|
|
HOME="$HOME" \
|
|
LEMANA_VPN_USERNAME="lemana-configure-$$" \
|
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
|
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
|
|
LEMANA_VPN_CAPTURE_TOTP="$captured_totp" \
|
|
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-keychain
|
|
)"
|
|
|
|
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
|
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
|
|
|
fake_pwd="$TMP_DIR/fake-pwd"
|
|
mkdir -p "$fake_pwd/bin"
|
|
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
|
|
|
piped_output="$(
|
|
cd "$fake_pwd" &&
|
|
LEMANA_VPN_RAW_BASE_URL="file://$ROOT" sh -s -- --dry-run --non-interactive --minimal --without-app < "$ROOT/install.sh"
|
|
)"
|
|
|
|
printf '%s\n' "$piped_output" | grep -q "curl -fsSL file://$ROOT/bin/vpn-lemanapro.sh"
|
|
if printf '%s\n' "$piped_output" | grep -q "$fake_pwd/bin/vpn-lemanapro.sh"; then
|
|
echo "piped install used stale PWD/bin/vpn-lemanapro.sh" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'smoke ok\n'
|