| 1 | #!/usr/bin/env sh |
| 2 | set -eu |
| 3 | |
| 4 | OS="$(uname -s)" |
| 5 | case "$OS" in |
| 6 | Linux) PORTAL_OS="linux" ;; |
| 7 | Darwin) PORTAL_OS="darwin" ;; |
| 8 | *) |
| 9 | echo "Unsupported OS: $OS" >&2 |
| 10 | exit 1 |
| 11 | ;; |
| 12 | esac |
| 13 | |
| 14 | ARCH="$(uname -m)" |
| 15 | case "$ARCH" in |
| 16 | x86_64|amd64) PORTAL_ARCH="amd64" ;; |
| 17 | arm64|aarch64) PORTAL_ARCH="arm64" ;; |
| 18 | *) |
| 19 | echo "Unsupported architecture: $ARCH" >&2 |
| 20 | exit 1 |
| 21 | ;; |
| 22 | esac |
| 23 | |
| 24 | BASE_URL="${BASE_URL:-https://github.com/gosuda/portal-tunnel/releases/latest/download}" |
| 25 | BIN_PATH_PREFIX="${BIN_PATH_PREFIX:-}" |
| 26 | BIN_SUFFIX="portal-$PORTAL_OS-$PORTAL_ARCH" |
| 27 | if [ -n "$BIN_PATH_PREFIX" ]; then |
| 28 | BIN_URL="${BIN_URL:-$BASE_URL/$BIN_PATH_PREFIX/$PORTAL_OS-$PORTAL_ARCH}" |
| 29 | else |
| 30 | BIN_URL="${BIN_URL:-$BASE_URL/$BIN_SUFFIX}" |
| 31 | fi |
| 32 | CHECKSUM_URL="${CHECKSUM_URL:-${BIN_URL}.sha256}" |
| 33 | RELAY_URL="${RELAY_URL:-https://your-relay.example.com}" |
| 34 | |
| 35 | is_local_https_url() { |
| 36 | case "$1" in |
| 37 | https://*) ;; |
| 38 | *) return 1 ;; |
| 39 | esac |
| 40 | |
| 41 | URL_HOSTPORT="${1#https://}" |
| 42 | URL_HOSTPORT="${URL_HOSTPORT%%/*}" |
| 43 | URL_HOSTPORT="${URL_HOSTPORT%%\?*}" |
| 44 | URL_HOSTPORT="${URL_HOSTPORT%%#*}" |
| 45 | |
| 46 | case "$URL_HOSTPORT" in |
| 47 | \[::1\]:*) |
| 48 | URL_HOST="[::1]" |
| 49 | URL_PORT="${URL_HOSTPORT#\[::1\]:}" |
| 50 | ;; |
| 51 | \[::1\]) |
| 52 | URL_HOST="[::1]" |
| 53 | URL_PORT="" |
| 54 | ;; |
| 55 | *:*) |
| 56 | URL_HOST="${URL_HOSTPORT%%:*}" |
| 57 | URL_PORT="${URL_HOSTPORT#*:}" |
| 58 | ;; |
| 59 | *) |
| 60 | URL_HOST="$URL_HOSTPORT" |
| 61 | URL_PORT="" |
| 62 | ;; |
| 63 | esac |
| 64 | |
| 65 | case "$URL_PORT" in |
| 66 | *[!0-9]*) return 1 ;; |
| 67 | esac |
| 68 | |
| 69 | case "$URL_HOST" in |
| 70 | localhost|127.0.0.1|\[::1\]|*.localhost) |
| 71 | return 0 |
| 72 | ;; |
| 73 | esac |
| 74 | return 1 |
| 75 | } |
| 76 | |
| 77 | download_url() { |
| 78 | if is_local_https_url "$1"; then |
| 79 | curl -k -fsSL "$1" -o "$2" |
| 80 | return |
| 81 | fi |
| 82 | curl -fsSL "$1" -o "$2" |
| 83 | } |
| 84 | |
| 85 | fetch_url() { |
| 86 | if is_local_https_url "$1"; then |
| 87 | curl -k -fsSL "$1" |
| 88 | return |
| 89 | fi |
| 90 | curl -fsSL "$1" |
| 91 | } |
| 92 | |
| 93 | TMPDIR="${TMPDIR:-/tmp}" |
| 94 | WORKDIR="$(mktemp -d "$TMPDIR/portal-install.XXXXXX" 2>/dev/null || mktemp -d -t portal-install)" |
| 95 | BIN_PATH="$WORKDIR/portal" |
| 96 | cleanup() { |
| 97 | if [ -n "${TMP_INSTALL_PATH:-}" ]; then |
| 98 | rm -f "$TMP_INSTALL_PATH" |
| 99 | fi |
| 100 | rm -rf "$WORKDIR" |
| 101 | } |
| 102 | trap cleanup EXIT INT TERM |
| 103 | |
| 104 | echo "Downloading portal ($PORTAL_OS/$PORTAL_ARCH)..." >&2 |
| 105 | download_url "$BIN_URL" "$BIN_PATH" |
| 106 | |
| 107 | echo "Verifying SHA256 checksum..." >&2 |
| 108 | CHECKSUM_PAYLOAD="$(fetch_url "$CHECKSUM_URL")" || { |
| 109 | echo "Failed to download checksum from $CHECKSUM_URL. Aborting (fail-closed)." >&2 |
| 110 | exit 1 |
| 111 | } |
| 112 | |
| 113 | EXPECTED_SHA="$(printf '%s\n' "$CHECKSUM_PAYLOAD" | awk '{print $1}' | tr 'A-Z' 'a-z')" |
| 114 | if ! printf '%s\n' "$EXPECTED_SHA" | grep -Eq '^[0-9a-f]{64}$'; then |
| 115 | echo "Invalid checksum payload from $CHECKSUM_URL. Aborting (fail-closed)." >&2 |
| 116 | echo "Hint: expected SHA256 sidecar format '<sha256> <filename>'." >&2 |
| 117 | exit 1 |
| 118 | fi |
| 119 | |
| 120 | if command -v sha256sum >/dev/null 2>&1; then |
| 121 | ACTUAL_SHA="$(sha256sum "$BIN_PATH" | awk '{print $1}')" |
| 122 | elif command -v shasum >/dev/null 2>&1; then |
| 123 | ACTUAL_SHA="$(shasum -a 256 "$BIN_PATH" | awk '{print $1}')" |
| 124 | else |
| 125 | echo "No SHA256 checksum tool found (need sha256sum or shasum)." >&2 |
| 126 | exit 1 |
| 127 | fi |
| 128 | if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then |
| 129 | echo "Checksum mismatch for portal binary. Aborting (fail-closed)." >&2 |
| 130 | exit 1 |
| 131 | fi |
| 132 | |
| 133 | pick_install_path() { |
| 134 | EXISTING="$(command -v portal 2>/dev/null || true)" |
| 135 | if [ -n "$EXISTING" ]; then |
| 136 | EXISTING_DIR="$(dirname "$EXISTING")" |
| 137 | if [ -d "$EXISTING_DIR" ] && [ -w "$EXISTING_DIR" ]; then |
| 138 | printf '%s\n' "$EXISTING" |
| 139 | return 0 |
| 140 | fi |
| 141 | fi |
| 142 | |
| 143 | if [ -n "${HOME:-}" ]; then |
| 144 | for DIR in "$HOME/.local/bin" "$HOME/bin"; do |
| 145 | mkdir -p "$DIR" 2>/dev/null || true |
| 146 | if [ -d "$DIR" ] && [ -w "$DIR" ]; then |
| 147 | printf '%s\n' "$DIR/portal" |
| 148 | return 0 |
| 149 | fi |
| 150 | done |
| 151 | fi |
| 152 | |
| 153 | return 1 |
| 154 | } |
| 155 | |
| 156 | INSTALL_PATH="$(pick_install_path)" || { |
| 157 | echo "No writable install directory found. Ensure an existing portal install is writable or create \$HOME/.local/bin or \$HOME/bin." >&2 |
| 158 | exit 1 |
| 159 | } |
| 160 | |
| 161 | INSTALL_DIR="$(dirname "$INSTALL_PATH")" |
| 162 | TMP_INSTALL_PATH="$(mktemp "$INSTALL_DIR/.portal.tmp.XXXXXX")" |
| 163 | cp "$BIN_PATH" "$TMP_INSTALL_PATH" |
| 164 | chmod +x "$TMP_INSTALL_PATH" |
| 165 | mv -f "$TMP_INSTALL_PATH" "$INSTALL_PATH" |
| 166 | TMP_INSTALL_PATH="" |
| 167 | |
| 168 | echo "Installed portal to $INSTALL_PATH" >&2 |
| 169 | |
| 170 | case ":$PATH:" in |
| 171 | *":$INSTALL_DIR:"*) ;; |
| 172 | *) |
| 173 | PROFILE_PATH="" |
| 174 | if [ -n "${HOME:-}" ]; then |
| 175 | SHELL_NAME="${SHELL:-}" |
| 176 | SHELL_NAME="${SHELL_NAME##*/}" |
| 177 | case "$SHELL_NAME" in |
| 178 | zsh) PROFILE_PATH="$HOME/.zshrc" ;; |
| 179 | bash) PROFILE_PATH="$HOME/.bashrc" ;; |
| 180 | *) PROFILE_PATH="$HOME/.profile" ;; |
| 181 | esac |
| 182 | fi |
| 183 | |
| 184 | if [ -n "$PROFILE_PATH" ]; then |
| 185 | mkdir -p "$(dirname "$PROFILE_PATH")" 2>/dev/null || true |
| 186 | if [ ! -f "$PROFILE_PATH" ] || ! grep -F "$INSTALL_DIR" "$PROFILE_PATH" >/dev/null 2>&1; then |
| 187 | { |
| 188 | printf '\n# Added by Portal installer\n' |
| 189 | printf 'export PATH="%s:$PATH"\n' "$INSTALL_DIR" |
| 190 | } >> "$PROFILE_PATH" || PROFILE_PATH="" |
| 191 | fi |
| 192 | fi |
| 193 | |
| 194 | if [ -n "$PROFILE_PATH" ]; then |
| 195 | echo "Added $INSTALL_DIR to PATH in $PROFILE_PATH" >&2 |
| 196 | echo "Open a new shell or run: export PATH=\"$INSTALL_DIR:\$PATH\"" >&2 |
| 197 | else |
| 198 | echo "Warning: $INSTALL_DIR is not on PATH. Add it before running 'portal expose 3000'." >&2 |
| 199 | fi |
| 200 | ;; |
| 201 | esac |
| 202 | |
| 203 | echo "Next step:" >&2 |
| 204 | echo " portal expose 3000 --relays $RELAY_URL" >&2 |