Files
lemana-vpn/install.sh

657 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
APP_NAME="lemana-vpn"
DEFAULT_RAW_BASE_URL="https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main"
RAW_BASE_URL="${LEMANA_VPN_RAW_BASE_URL:-$DEFAULT_RAW_BASE_URL}"
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
USE_BITWARDEN=1
USE_TOUCHID=1
INSTALL_SUDOERS=1
INSTALL_ALIASES=1
INSTALL_APP=1
INSTALL_AUTOSTART=1
CONFIGURE_KEYCHAIN=0
DRY_RUN=0
FORCE=0
INTERACTIVE=auto
BITWARDEN_FORCED=0
TOUCHID_FORCED=0
SUDOERS_FORCED=0
SHELL_FORCED=0
APP_FORCED=0
AUTOSTART_FORCED=0
CONFIGURE_KEYCHAIN_FORCED=0
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
usage() {
cat <<'USAGE'
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
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
--without-touchid Do not install/use Touch ID helper
--configure-keychain Prompt for LDAP password and TOTP secret after install
--username VALUE Corporate LDAP username (default: 60103293)
--bw-item VALUE Bitwarden item name (default: LM LDAP)
--raw-base-url URL Raw file base URL for curl installs
--no-sudoers Do not install sudoers rules
--no-shell Do not update ~/.zshrc aliases
--with-app Build/install macOS menu bar app (default)
--without-app Do not build/install macOS menu bar app
--with-autostart Install LaunchAgent for menu bar app (default)
--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
--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 --without-touchid
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--with-bitwarden)
USE_BITWARDEN=1
BITWARDEN_FORCED=1
;;
--without-bitwarden)
USE_BITWARDEN=0
BITWARDEN_FORCED=1
;;
--with-touchid)
USE_TOUCHID=1
TOUCHID_FORCED=1
;;
--without-touchid)
USE_TOUCHID=0
TOUCHID_FORCED=1
;;
--configure-keychain)
CONFIGURE_KEYCHAIN=1
CONFIGURE_KEYCHAIN_FORCED=1
;;
--username)
shift
[ "$#" -gt 0 ] || { echo "--username requires a value" >&2; exit 1; }
USERNAME="$1"
;;
--bw-item)
shift
[ "$#" -gt 0 ] || { echo "--bw-item requires a value" >&2; exit 1; }
BW_ITEM="$1"
;;
--raw-base-url)
shift
[ "$#" -gt 0 ] || { echo "--raw-base-url requires a value" >&2; exit 1; }
RAW_BASE_URL="${1%/}"
;;
--no-sudoers)
INSTALL_SUDOERS=0
SUDOERS_FORCED=1
;;
--no-shell)
INSTALL_ALIASES=0
SHELL_FORCED=1
;;
--with-app)
INSTALL_APP=1
APP_FORCED=1
;;
--without-app)
INSTALL_APP=0
INSTALL_AUTOSTART=0
APP_FORCED=1
AUTOSTART_FORCED=1
;;
--with-autostart)
INSTALL_AUTOSTART=1
AUTOSTART_FORCED=1
;;
--without-autostart)
INSTALL_AUTOSTART=0
AUTOSTART_FORCED=1
;;
--interactive) INTERACTIVE=1 ;;
--non-interactive) INTERACTIVE=0 ;;
--minimal)
USE_BITWARDEN=0
USE_TOUCHID=0
BITWARDEN_FORCED=1
TOUCHID_FORCED=1
;;
--dry-run) DRY_RUN=1 ;;
--force) FORCE=1 ;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
log() {
printf '%s\n' "$*"
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
run() {
if [ "$DRY_RUN" -eq 1 ]; then
printf '+'
for arg in "$@"; do
printf ' %s' "$arg"
done
printf '\n'
return 0
fi
"$@"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Command not found: $1"
}
has_tty() {
[ -r /dev/tty ] && [ -w /dev/tty ]
}
interactive_enabled() {
case "$INTERACTIVE" in
1) has_tty ;;
0) return 1 ;;
auto) has_tty ;;
*) return 1 ;;
esac
}
yes_no() {
prompt="$1"
default_answer="$2"
[ "$default_answer" = "y" ] || [ "$default_answer" = "n" ] || die "Invalid yes_no default: $default_answer"
if ! interactive_enabled; then
[ "$default_answer" = "y" ]
return $?
fi
if [ "$default_answer" = "y" ]; then
suffix="[Y/n]"
else
suffix="[y/N]"
fi
while :; do
printf '%s %s ' "$prompt" "$suffix" > /dev/tty
IFS= read -r answer < /dev/tty || answer=""
case "$answer" in
"") [ "$default_answer" = "y" ]; return $? ;;
y|Y|yes|YES|Yes|д|Д|да|Да|ДА) return 0 ;;
n|N|no|NO|No|н|Н|нет|Нет|НЕТ) return 1 ;;
*) printf 'Введите y или n.\n' > /dev/tty ;;
esac
done
}
bool_word() {
if "$@" >/dev/null 2>&1; then
printf 'yes'
else
printf 'no'
fi
}
keychain_has() {
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
}
zsh_aliases_installed() {
[ -f "$HOME/.zshrc" ] && grep -q '^# >>> lemana-vpn$' "$HOME/.zshrc"
}
print_detected_state() {
log "Detected state:"
log " openconnect: $(bool_word command -v openconnect)"
log " pipx: $(bool_word command -v pipx)"
log " openconnect-lite: $(bool_word test -x "$HOME/.local/bin/openconnect-lite")"
log " Bitwarden CLI: $(bool_word command -v bw)"
log " Touch ID helper: $(bool_word test -x "$INSTALL_BIN_DIR/keychain-fingerprint")"
log " DNS cleanup: $(bool_word test -x "$DNS_CLEANUP")"
log " sudoers: $(bool_word test -f /etc/sudoers.d/lemana-vpn-openconnect)/$(bool_word test -f /etc/sudoers.d/lemana-vpn-dns)"
log " shell aliases: $(bool_word zsh_aliases_installed)"
log " Swift: $(bool_word command -v swift)"
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")"
}
choose_modules() {
print_detected_state
if ! interactive_enabled; then
log "Interactive prompts: off"
return 0
fi
log "Interactive prompts: on"
if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
USE_BITWARDEN=1
else
USE_BITWARDEN=0
fi
fi
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
USE_TOUCHID=1
else
USE_TOUCHID=0
fi
fi
else
USE_TOUCHID=0
fi
fi
if [ "$SUDOERS_FORCED" -eq 0 ]; then
if ! [ -f /etc/sudoers.d/lemana-vpn-openconnect ] || ! [ -f /etc/sudoers.d/lemana-vpn-dns ]; then
if yes_no "Настроить sudoers для VPN/DNS без повторного sudo-пароля?" y; then
INSTALL_SUDOERS=1
else
INSTALL_SUDOERS=0
fi
fi
fi
if [ "$SHELL_FORCED" -eq 0 ] && ! zsh_aliases_installed; then
if yes_no "Добавить алиасы vpn/vpn-debug/vpn-fix-dns в ~/.zshrc?" y; then
INSTALL_ALIASES=1
else
INSTALL_ALIASES=0
fi
fi
if [ "$APP_FORCED" -eq 0 ] && ! [ -x "$APP_DIR/Contents/MacOS/LemanaVPN" ]; then
if yes_no "Swift Menu Bar app не найден. Собрать и установить LemanaVPN.app?" y; then
INSTALL_APP=1
else
INSTALL_APP=0
INSTALL_AUTOSTART=0
fi
fi
if [ "$AUTOSTART_FORCED" -eq 0 ] && [ "$INSTALL_APP" -eq 1 ] && ! [ -f "$LAUNCH_AGENT" ]; then
if yes_no "Включить автозапуск LemanaVPN.app при логине?" y; then
INSTALL_AUTOSTART=1
else
INSTALL_AUTOSTART=0
fi
fi
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$USE_BITWARDEN" -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
CONFIGURE_KEYCHAIN=1
fi
fi
fi
}
script_dir() {
case "$0" in
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
*) pwd ;;
esac
}
download_file() {
src="$1"
dst="$2"
local_dir="$(script_dir)"
if [ -f "$local_dir/$src" ]; then
run cp "$local_dir/$src" "$dst"
return 0
fi
if [ -f "$PWD/$src" ]; then
run cp "$PWD/$src" "$dst"
return 0
fi
need_cmd curl
run curl -fsSL "$RAW_BASE_URL/$src" -o "$dst"
}
write_file() {
dst="$1"
content="$2"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ write %s\n' "$dst"
return 0
fi
printf '%s\n' "$content" > "$dst"
}
install_homebrew_packages() {
need_cmd brew
for pkg in openconnect pipx; do
if brew list "$pkg" >/dev/null 2>&1; then
log "Homebrew package already installed: $pkg"
else
log "Installing Homebrew package: $pkg"
run brew install "$pkg"
fi
done
if [ "$USE_BITWARDEN" -eq 1 ]; then
if brew list bitwarden-cli >/dev/null 2>&1; then
log "Homebrew package already installed: bitwarden-cli"
else
log "Installing Homebrew package: bitwarden-cli"
run brew install bitwarden-cli
fi
fi
}
install_openconnect_lite() {
need_cmd pipx
if [ -x "$HOME/.local/bin/openconnect-lite" ] && [ "$FORCE" -eq 0 ]; then
log "openconnect-lite already installed"
else
log "Installing openconnect-lite via pipx"
run pipx install openconnect-lite
fi
if pipx --help 2>/dev/null | grep -q ' pin '; then
run pipx pin openconnect-lite >/dev/null 2>&1 || true
fi
}
install_cli() {
tmp="$1"
run mkdir -p "$INSTALL_BIN_DIR"
download_file "bin/vpn-lemanapro.sh" "$tmp/vpn-lemanapro.sh"
run install -m 755 "$tmp/vpn-lemanapro.sh" "$INSTALL_BIN_DIR/vpn-lemanapro.sh"
download_file "uninstall.sh" "$tmp/uninstall.sh"
run install -m 755 "$tmp/uninstall.sh" "$INSTALL_BIN_DIR/uninstall-lemana-vpn.sh"
}
install_config() {
tmp="$1"
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
download_file "templates/openconnect-lite-config.toml" "$tmp/openconnect-lite-config.toml"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ render %s/config.toml\n' "$OC_CONFIG_DIR"
else
sed "s/{{USERNAME}}/$USERNAME/g" "$tmp/openconnect-lite-config.toml" > "$OC_CONFIG_DIR/config.toml"
chmod 600 "$OC_CONFIG_DIR/config.toml"
fi
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\""
write_file "$tmp/env" "$env_content"
run install -m 600 "$tmp/env" "$CONFIG_DIR/env"
}
install_dns_cleanup() {
tmp="$1"
dns_cleanup_dir="$(dirname "$DNS_CLEANUP")"
download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup"
run sudo install -d -m 755 -o root -g wheel "$dns_cleanup_dir"
log "Installing DNS cleanup wrapper: $DNS_CLEANUP"
run sudo install -m 755 -o root -g wheel "$tmp/lemana-vpn-dns-cleanup" "$DNS_CLEANUP"
}
install_sudoers() {
[ "$INSTALL_SUDOERS" -eq 1 ] || return 0
openconnect_bin="$(brew --prefix)/bin/openconnect"
[ -x "$openconnect_bin" ] || openconnect_bin="$(command -v openconnect || true)"
[ -n "$openconnect_bin" ] || die "openconnect binary not found"
tmp="$1"
current_user="$(id -un)"
run sudo install -d -m 755 -o root -g wheel /etc/sudoers.d
write_file "$tmp/sudoers-openconnect" "$current_user ALL=(ALL) NOPASSWD: $openconnect_bin"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-openconnect" /etc/sudoers.d/lemana-vpn-openconnect
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-openconnect
write_file "$tmp/sudoers-dns" "$current_user ALL=(ALL) NOPASSWD: $DNS_CLEANUP"
run sudo install -m 440 -o root -g wheel "$tmp/sudoers-dns" /etc/sudoers.d/lemana-vpn-dns
run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-dns
}
install_touchid_helper() {
[ "$USE_TOUCHID" -eq 1 ] || return 0
if [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ] && [ "$FORCE" -eq 0 ]; then
log "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint"
return 0
fi
need_cmd git
need_cmd swiftc
tmp="$1"
log "Building keychain-fingerprint helper"
run git clone --depth 1 https://github.com/dss99911/keychain-fingerprint.git "$tmp/keychain-fingerprint"
run swiftc -o "$tmp/keychain-fingerprint-bin" "$tmp/keychain-fingerprint/main.swift" -framework LocalAuthentication -framework Security
run install -m 700 "$tmp/keychain-fingerprint-bin" "$INSTALL_BIN_DIR/keychain-fingerprint"
}
install_menu_bar_app() {
[ "$INSTALL_APP" -eq 1 ] || return 0
need_cmd swift
tmp="$1"
app_src="$tmp/app"
run mkdir -p "$app_src/Sources/LemanaVPN"
download_file "app/Package.swift" "$app_src/Package.swift"
download_file "app/Sources/LemanaVPN/LemanaVPNApp.swift" "$app_src/Sources/LemanaVPN/LemanaVPNApp.swift"
download_file "app/Sources/LemanaVPN/VPNManager.swift" "$app_src/Sources/LemanaVPN/VPNManager.swift"
log "Building LemanaVPN.app"
run swift build -c release --package-path "$app_src"
app_bin="$app_src/.build/release/LemanaVPN"
info_plist="$tmp/Info.plist"
if [ "$DRY_RUN" -eq 0 ] && [ ! -x "$app_bin" ]; then
die "Swift build did not produce $app_bin"
fi
run mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
run install -m 755 "$app_bin" "$APP_DIR/Contents/MacOS/LemanaVPN"
write_file "$info_plist" '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>LemanaVPN</string>
<key>CFBundleIdentifier</key>
<string>ru.dokops.LemanaVPN</string>
<key>CFBundleName</key>
<string>LemanaVPN</string>
<key>CFBundleDisplayName</key>
<string>LemanaVPN</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>'
run install -m 644 "$info_plist" "$APP_DIR/Contents/Info.plist"
}
install_launch_agent() {
[ "$INSTALL_AUTOSTART" -eq 1 ] || return 0
[ "$INSTALL_APP" -eq 1 ] || return 0
tmp="$1"
plist="$tmp/ru.dokops.LemanaVPN.plist"
run mkdir -p "$HOME/Library/LaunchAgents"
write_file "$plist" "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>Label</key>
<string>ru.dokops.LemanaVPN</string>
<key>ProgramArguments</key>
<array>
<string>$APP_DIR/Contents/MacOS/LemanaVPN</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>"
run install -m 644 "$plist" "$LAUNCH_AGENT"
if [ "$DRY_RUN" -eq 0 ]; then
launchctl unload "$LAUNCH_AGENT" >/dev/null 2>&1 || true
launchctl load "$LAUNCH_AGENT" >/dev/null 2>&1 || true
else
printf '+ launchctl load %s\n' "$LAUNCH_AGENT"
fi
}
install_shell_aliases() {
[ "$INSTALL_ALIASES" -eq 1 ] || return 0
zshrc="$HOME/.zshrc"
tmp="$1"
[ -f "$zshrc" ] || run touch "$zshrc"
block="$tmp/zshrc-block"
if [ "$DRY_RUN" -eq 1 ]; then
printf '+ update %s aliases\n' "$zshrc"
return 0
fi
cat > "$block" <<EOF
# >>> lemana-vpn
vpn() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" "\$@"; }
vpn-debug() { "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --debug "\$@"; }
vpn-fix-dns() { sudo "$DNS_CLEANUP"; }
# <<< lemana-vpn
EOF
awk '
/^# >>> lemana-vpn$/ { skip=1; next }
/^# <<< lemana-vpn$/ { skip=0; next }
skip != 1 { print }
' "$zshrc" > "$tmp/zshrc"
{
cat "$tmp/zshrc"
printf '\n'
cat "$block"
} > "$tmp/zshrc.new"
mv "$tmp/zshrc.new" "$zshrc"
}
maybe_login_bitwarden() {
[ "$USE_BITWARDEN" -eq 1 ] || return 0
command -v bw >/dev/null 2>&1 || return 0
status="$(bw status 2>/dev/null || true)"
if printf '%s\n' "$status" | grep -q '"status":"unauthenticated"'; then
log "Bitwarden CLI is not logged in. Run later: bw login"
elif printf '%s\n' "$status" | grep -q '"status":"locked"'; then
log "Bitwarden CLI is logged in but locked. First vpn run will ask for master password."
else
log "Bitwarden CLI is available."
fi
}
main() {
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT INT TERM
choose_modules
log "Installing Lemana VPN"
log "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
install_homebrew_packages
install_openconnect_lite
install_cli "$tmp"
install_config "$tmp"
install_dns_cleanup "$tmp"
install_sudoers "$tmp"
install_touchid_helper "$tmp"
install_menu_bar_app "$tmp"
install_launch_agent "$tmp"
install_shell_aliases "$tmp"
maybe_login_bitwarden
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
fi
log ""
log "Done."
log "Open a new shell or run: exec zsh"
log "Connect: vpn"
log "Status: vpn --status"
if [ "$INSTALL_APP" -eq 1 ]; then
log "App: open '$APP_DIR'"
fi
}
main "$@"