358 lines
9.4 KiB
Bash
Executable File
358 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INSTALL_DIR="${VPN_PROXY_INSTALL_DIR:-$HOME/.vpn-proxy-client}"
|
|
BRANCH="${VPN_PROXY_BRANCH:-master}"
|
|
ARCHIVE_URL="${VPN_PROXY_ARCHIVE_URL:-https://git.dokops.ru/dokril/vpn-proxy/archive/${BRANCH}.tar.gz}"
|
|
SOURCE_DIR="${VPN_PROXY_SOURCE_DIR:-}"
|
|
COMPOSE_FILE="docker-compose.client.yml"
|
|
DEFAULT_PROXY_PORT="8082"
|
|
REQUESTED_PROXY_PORT="${VPN_PROXY_CLIENT_PORT:-}"
|
|
REQUESTED_UI_PORT="${VPN_PROXY_CLIENT_UI_PORT:-${CLIENT_UI_PORT:-}}"
|
|
CLIENT_CONTAINER_NAME="harbor-connect"
|
|
LEGACY_CLIENT_CONTAINER_NAME="vpn-proxy-client"
|
|
NETWORK_MONITOR_LABEL="com.dokril.harbor-connect.network"
|
|
|
|
log() {
|
|
printf '[harbor-connect] %s\n' "$*"
|
|
}
|
|
|
|
die() {
|
|
printf '[harbor-connect] error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
need() {
|
|
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
|
|
}
|
|
|
|
is_valid_port() {
|
|
case "$1" in
|
|
''|*[!0-9]*) return 1 ;;
|
|
esac
|
|
[ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
|
|
}
|
|
|
|
ask_proxy_port() {
|
|
local value=""
|
|
if [ -n "$REQUESTED_PROXY_PORT" ]; then
|
|
if ! is_valid_port "$REQUESTED_PROXY_PORT"; then
|
|
die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
|
|
fi
|
|
printf '%s\n' "$REQUESTED_PROXY_PORT"
|
|
return 0
|
|
fi
|
|
|
|
if [ -r /dev/tty ]; then
|
|
while true; do
|
|
printf 'Proxy port for local apps [%s]: ' "$DEFAULT_PROXY_PORT" >/dev/tty
|
|
IFS= read -r value </dev/tty || value=""
|
|
value="${value:-$DEFAULT_PROXY_PORT}"
|
|
if is_valid_port "$value"; then
|
|
printf '%s\n' "$value"
|
|
return 0
|
|
fi
|
|
printf 'Enter a port from 1024 to 65535.\n' >/dev/tty
|
|
done
|
|
fi
|
|
|
|
if ! is_valid_port "$DEFAULT_PROXY_PORT"; then
|
|
die "VPN_PROXY_CLIENT_PORT must be a port from 1024 to 65535"
|
|
fi
|
|
printf '%s\n' "$DEFAULT_PROXY_PORT"
|
|
}
|
|
|
|
published_port_conflicts() {
|
|
local port="$1"
|
|
local line
|
|
|
|
while IFS= read -r line; do
|
|
[ -n "$line" ] || continue
|
|
case "$line" in
|
|
"${CLIENT_CONTAINER_NAME}"$'\t'*|"${LEGACY_CLIENT_CONTAINER_NAME}"$'\t'*) ;;
|
|
*) printf '%s\n' "$line" ;;
|
|
esac
|
|
done < <(docker ps --filter "publish=${port}" --format '{{.Names}} {{.Ports}}')
|
|
}
|
|
|
|
proxy_port_conflicts() {
|
|
published_port_conflicts "$1"
|
|
}
|
|
|
|
assert_proxy_port_available() {
|
|
local port="$1"
|
|
local conflicts
|
|
|
|
conflicts="$(proxy_port_conflicts "$port")"
|
|
if [ -z "$conflicts" ]; then
|
|
return 0
|
|
fi
|
|
|
|
printf '[harbor-connect] proxy port %s is already used:\n%s\n' \
|
|
"$port" "$conflicts" >&2
|
|
die "choose another proxy port with VPN_PROXY_CLIENT_PORT=<port> or stop the conflicting container"
|
|
}
|
|
|
|
assert_single_port_available() {
|
|
local label="$1"
|
|
local port="$2"
|
|
local conflicts
|
|
|
|
conflicts="$(published_port_conflicts "$port")"
|
|
if [ -z "$conflicts" ]; then
|
|
return 0
|
|
fi
|
|
|
|
printf '[harbor-connect] %s port %s is already used:\n%s\n' \
|
|
"$label" "$port" "$conflicts" >&2
|
|
die "choose another ${label} port or stop the conflicting container"
|
|
}
|
|
|
|
first_free_port() {
|
|
local start="$1"
|
|
local port
|
|
|
|
for port in $(seq "$start" 65535); do
|
|
if [ -z "$(published_port_conflicts "$port")" ]; then
|
|
printf '%s\n' "$port"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
choose_ui_port() {
|
|
local value="$1"
|
|
local suggested
|
|
|
|
if ! is_valid_port "$value"; then
|
|
die "CLIENT_UI_PORT must be a port from 1024 to 65535"
|
|
fi
|
|
|
|
if [ -z "$(published_port_conflicts "$value")" ]; then
|
|
printf '%s\n' "$value"
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "$REQUESTED_UI_PORT" ] || [ ! -r /dev/tty ]; then
|
|
assert_single_port_available "UI" "$value"
|
|
fi
|
|
|
|
suggested="$(first_free_port "$((value + 1))" || true)"
|
|
suggested="${suggested:-3457}"
|
|
while true; do
|
|
printf 'UI port %s is busy. Choose UI port [%s]: ' "$value" "$suggested" >/dev/tty
|
|
IFS= read -r value </dev/tty || value=""
|
|
value="${value:-$suggested}"
|
|
if is_valid_port "$value" && [ -z "$(published_port_conflicts "$value")" ]; then
|
|
printf '%s\n' "$value"
|
|
return 0
|
|
fi
|
|
printf 'Enter a free port from 1024 to 65535.\n' >/dev/tty
|
|
done
|
|
}
|
|
|
|
assert_ui_outside_proxy_range() {
|
|
if [ "$UI_PORT" = "$PROXY_PORT" ]; then
|
|
die "UI port ${UI_PORT} overlaps proxy port"
|
|
fi
|
|
}
|
|
|
|
wait_for_client_ui() {
|
|
local ui_port="${UI_PORT:-3456}"
|
|
local ui_url="http://127.0.0.1:${ui_port}/api/state"
|
|
local attempt
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if curl --noproxy "*" -fsS "$ui_url" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
printf '\n[harbor-connect] client did not become ready at %s\n' "$ui_url" >&2
|
|
printf '[harbor-connect] docker compose status:\n' >&2
|
|
docker compose -f "$COMPOSE_FILE" ps >&2 || true
|
|
printf '\n[harbor-connect] recent service logs:\n' >&2
|
|
docker compose -f "$COMPOSE_FILE" logs --tail=120 harbor-connect >&2 || true
|
|
die "client UI is not ready; see Docker status and logs above"
|
|
}
|
|
|
|
xml_escape() {
|
|
sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g'
|
|
}
|
|
|
|
install_network_monitor() {
|
|
local launch_agents_dir="$HOME/Library/LaunchAgents"
|
|
local plist_path="$launch_agents_dir/${NETWORK_MONITOR_LABEL}.plist"
|
|
local escaped_script_path
|
|
local escaped_runtime_dir
|
|
local user_domain="gui/$(id -u)"
|
|
|
|
escaped_script_path="$(printf '%s' "$INSTALL_DIR/scripts/harbor-network-monitor.sh" | xml_escape)"
|
|
escaped_runtime_dir="$(printf '%s' "$INSTALL_DIR/.runtime" | xml_escape)"
|
|
mkdir -p "$INSTALL_DIR/.runtime" "$launch_agents_dir"
|
|
|
|
cat > "$plist_path" <<EOF
|
|
<?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>${NETWORK_MONITOR_LABEL}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/bin/bash</string>
|
|
<string>${escaped_script_path}</string>
|
|
</array>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>HARBOR_RUNTIME_DIR</key>
|
|
<string>${escaped_runtime_dir}</string>
|
|
</dict>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>StartInterval</key>
|
|
<integer>5</integer>
|
|
<key>ProcessType</key>
|
|
<string>Background</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
/bin/bash "$INSTALL_DIR/scripts/harbor-network-monitor.sh"
|
|
launchctl bootout "$user_domain" "$plist_path" >/dev/null 2>&1 || true
|
|
launchctl bootstrap "$user_domain" "$plist_path"
|
|
log "automatic Gateway detection enabled"
|
|
}
|
|
|
|
set_env_value() {
|
|
local key="$1"
|
|
local value="$2"
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
|
|
if [ -f .env ] && grep -q "^${key}=" .env; then
|
|
awk -v key="$key" -v value="$value" '
|
|
BEGIN { prefix = key "=" }
|
|
index($0, prefix) == 1 { print key "=" value; next }
|
|
{ print }
|
|
' .env > "$tmp"
|
|
else
|
|
[ -f .env ] && cat .env > "$tmp"
|
|
printf '%s=%s\n' "$key" "$value" >> "$tmp"
|
|
fi
|
|
|
|
mv "$tmp" .env
|
|
}
|
|
|
|
get_env_value() {
|
|
local key="$1"
|
|
[ -f .env ] || return 0
|
|
awk -v key="$key" '
|
|
BEGIN { prefix = key "=" }
|
|
index($0, prefix) == 1 { print substr($0, length(prefix) + 1); exit }
|
|
' .env
|
|
}
|
|
|
|
copy_source() {
|
|
local source_dir="$1"
|
|
[ -f "$source_dir/docker-compose.client.yml" ] || die "invalid Harbor source archive"
|
|
log "installing files to $INSTALL_DIR"
|
|
mkdir -p "$INSTALL_DIR"
|
|
cp -R "$source_dir/." "$INSTALL_DIR/"
|
|
}
|
|
|
|
download_source() {
|
|
local tmp_dir
|
|
tmp_dir="$(mktemp -d)"
|
|
mkdir -p "$tmp_dir/source"
|
|
log "downloading $ARCHIVE_URL"
|
|
if ! curl -fsSL "$ARCHIVE_URL" | tar -xzf - -C "$tmp_dir/source" --strip-components=1; then
|
|
rm -rf "$tmp_dir"
|
|
die "failed to download Harbor source"
|
|
fi
|
|
copy_source "$tmp_dir/source"
|
|
rm -rf "$tmp_dir"
|
|
}
|
|
|
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
die "this installer is intended for macOS"
|
|
fi
|
|
|
|
need docker
|
|
need curl
|
|
need tar
|
|
|
|
docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required"
|
|
docker info >/dev/null 2>&1 || die "Docker Desktop is not running"
|
|
|
|
if [[ -n "$SOURCE_DIR" ]]; then
|
|
copy_source "$SOURCE_DIR"
|
|
elif [[ -d "$INSTALL_DIR/.git" ]]; then
|
|
need git
|
|
log "updating $INSTALL_DIR"
|
|
git -C "$INSTALL_DIR" fetch origin "$BRANCH"
|
|
git -C "$INSTALL_DIR" checkout "$BRANCH"
|
|
git -C "$INSTALL_DIR" pull --ff-only origin "$BRANCH"
|
|
else
|
|
mkdir -p "$(dirname "$INSTALL_DIR")"
|
|
download_source
|
|
fi
|
|
|
|
cd "$INSTALL_DIR"
|
|
|
|
if [[ ! -f .env && -f .env.example ]]; then
|
|
cp .env.example .env
|
|
fi
|
|
|
|
PROXY_PORT="$(ask_proxy_port)"
|
|
assert_proxy_port_available "$PROXY_PORT"
|
|
UI_PORT="${REQUESTED_UI_PORT:-$(get_env_value CLIENT_UI_PORT)}"
|
|
UI_PORT="${UI_PORT:-3456}"
|
|
UI_PORT="$(choose_ui_port "$UI_PORT")"
|
|
assert_ui_outside_proxy_range
|
|
|
|
set_env_value APP_MODE client
|
|
set_env_value CLIENT_UI_PORT "$UI_PORT"
|
|
set_env_value CLIENT_PROXY_PORT "$PROXY_PORT"
|
|
set_env_value PROXY_PORT "$PROXY_PORT"
|
|
|
|
log "UI port: http://127.0.0.1:${UI_PORT}"
|
|
log "proxy port: 127.0.0.1:${PROXY_PORT}"
|
|
|
|
install_network_monitor
|
|
|
|
log "building and starting Docker client"
|
|
docker compose -f "$COMPOSE_FILE" up -d --build --remove-orphans
|
|
wait_for_client_ui
|
|
|
|
cat <<EOF
|
|
|
|
Harbor Connect is running.
|
|
|
|
UI:
|
|
http://127.0.0.1:${UI_PORT}
|
|
|
|
Proxy:
|
|
HTTP/SOCKS5 127.0.0.1:${PROXY_PORT}
|
|
This is the only Docker-published proxy port. Re-run the installer with VPN_PROXY_CLIENT_PORT=<port> to change it.
|
|
|
|
Useful commands:
|
|
cd ~/.vpn-proxy-client
|
|
docker compose -f docker-compose.client.yml logs -f
|
|
docker compose -f docker-compose.client.yml restart
|
|
docker compose -f docker-compose.client.yml down
|
|
|
|
Optional macOS system proxy example:
|
|
networksetup -setwebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
|
networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
|
networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 ${PROXY_PORT}
|
|
|
|
Disable later:
|
|
networksetup -setwebproxystate Wi-Fi off
|
|
networksetup -setsecurewebproxystate Wi-Fi off
|
|
networksetup -setsocksfirewallproxystate Wi-Fi off
|
|
|
|
EOF
|