#!/usr/bin/env sh
# tunnelto install script.
#
# Detects OS + architecture, downloads the matching binary, installs to
# $HOME/.local/bin/tunnelto (override with TUNNELTO_INSTALL_DIR), and on
# macOS strips quarantine xattrs and re-applies an ad-hoc signature so
# AMFI/Gatekeeper don't SIGKILL the binary — a recent behavior on macOS
# 14.4+ that turned working installs into a silent "killed" with no
# diagnostic.
#
# Usage:
#   curl -fsSL https://app.tunnelto.me/install.sh | sh
#   wget -qO-  https://app.tunnelto.me/install.sh | sh
#
# Pre-fill the access token so first-run setup doesn't have to ask for it
# (the `-s --` hands the args to this script rather than to sh):
#   curl -fsSL https://app.tunnelto.me/install.sh | sh -s -- -t <token>
#
# Env var overrides:
#   TUNNELTO_SERVER       base URL to download from (default: https://app.tunnelto.me)
#   TUNNELTO_INSTALL_DIR  install target (default: $HOME/.local/bin)
#   TUNNELTO_TOKEN        access token for first-run setup (or pass -t/--token)
#   TUNNELTO_REPORT_ERRORS  0 = never send install failure reports (default: ask)

set -eu

TUNNELTO_SERVER="${TUNNELTO_SERVER:-https://app.tunnelto.me}"
INSTALL_DIR="${TUNNELTO_INSTALL_DIR:-$HOME/.local/bin}"
TUNNELTO_TOKEN="${TUNNELTO_TOKEN:-}"

# Optional access token from args. Accepts `-t <token>`, `--token <token>`,
# or `--token=<token>`; the env var above is the fallback. With a token,
# first-run setup skips the "go fetch a token from the dashboard" detour —
# it still asks (interactively) for a machine name, defaulting to the hostname.
while [ $# -gt 0 ]; do
    case "$1" in
        -t|--token) TUNNELTO_TOKEN="${2:-}"; shift 2 || shift ;;
        --token=*)  TUNNELTO_TOKEN="${1#--token=}"; shift ;;
        --)         shift ;;
        *)          echo "warning: ignoring unknown install argument: $1" >&2; shift ;;
    esac
done

# Pick a downloader once. Minimal Ubuntu/Debian images (cloud, container,
# fresh Parallels VM) ship with wget but not curl, so requiring curl turns
# a one-liner into a "first install curl" detour for newcomers.
if command -v curl >/dev/null 2>&1; then
    download() {
        if [ -n "$TUNNELTO_TOKEN" ]; then
            curl -fSL --progress-bar -H "Authorization: Bearer $TUNNELTO_TOKEN" "$1" -o "$2"
        else
            curl -fSL --progress-bar "$1" -o "$2"
        fi
    }
    send_report() {
        if [ -n "$TUNNELTO_TOKEN" ]; then
            curl -fsS -m 5 -X POST \
                -H "Authorization: Bearer $TUNNELTO_TOKEN" \
                --data-urlencode "stage=$1" \
                --data-urlencode "os=${os:-}" \
                --data-urlencode "arch=${arch:-}" \
                "$TUNNELTO_SERVER/api/install-report" >/dev/null 2>&1 || true
        else
            curl -fsS -m 5 -X POST \
                --data-urlencode "stage=$1" \
                --data-urlencode "os=${os:-}" \
                --data-urlencode "arch=${arch:-}" \
                "$TUNNELTO_SERVER/api/install-report" >/dev/null 2>&1 || true
        fi
    }
elif command -v wget >/dev/null 2>&1; then
    download() {
        if [ -n "$TUNNELTO_TOKEN" ]; then
            wget --header="Authorization: Bearer $TUNNELTO_TOKEN" -O "$2" "$1"
        else
            wget -O "$2" "$1"
        fi
    }
    send_report() {
        if [ -n "$TUNNELTO_TOKEN" ]; then
            wget -q -T 5 -O /dev/null \
                --header="Authorization: Bearer $TUNNELTO_TOKEN" \
                --post-data "stage=$1&os=${os:-}&arch=${arch:-}" \
                "$TUNNELTO_SERVER/api/install-report" >/dev/null 2>&1 || true
        else
            wget -q -T 5 -O /dev/null \
                --post-data "stage=$1&os=${os:-}&arch=${arch:-}" \
                "$TUNNELTO_SERVER/api/install-report" >/dev/null 2>&1 || true
        fi
    }
else
    cat >&2 <<'EOF'
error: neither curl nor wget is installed.

tunnelto's installer needs one of them to fetch the binary. Install
either, then re-run:

    Debian/Ubuntu:  sudo apt-get install -y curl
    Fedora/RHEL:    sudo dnf install -y curl
    Alpine:         sudo apk add curl
EOF
    exit 1
fi

# Ask before sending anything. Nothing about a failed install leaves the
# machine unless the person at the keyboard says yes to the exact contents.
#
# Under `curl | sh` this script's own stdin is the curl pipe, so the prompt
# reads from /dev/tty — the same mechanism first-run setup uses below. With no
# controlling terminal (CI, containers, provisioning scripts) there is nobody
# to ask, so nothing is sent. Silence always means no.
#
# TUNNELTO_REPORT_ERRORS=0 declines without prompting. =1 answers yes in
# advance — deliberately not advertised in the usage header above; it exists so
# support can ask someone actively debugging with us to pre-authorize, not as a
# general way to turn reporting on.
post_report() {
    if [ "${TUNNELTO_REPORT_ERRORS:-}" = "0" ]; then
        return 0
    fi

    if [ "${TUNNELTO_REPORT_ERRORS:-}" = "1" ]; then
        send_report "$1"
        return 0
    fi

    # No controlling terminal → no one to consent → send nothing. Testing by
    # opening it, not just checking that the device node exists: /dev/tty is
    # present but unopenable when the process has no controlling terminal.
    { : < /dev/tty; } 2>/dev/null || return 0

    if [ -n "$TUNNELTO_TOKEN" ]; then
        report_account="your tunnelto account (identified by the access token you passed)"
    else
        report_account="not included (this report would be anonymous)"
    fi

    {
        echo ""
        echo "  The install failed. We'd like to send the error to our support team so"
        echo "  we can fix it. Nothing is sent unless you say yes."
        echo ""
        echo "  This is everything we would send to $TUNNELTO_SERVER:"
        echo ""
        echo "      what failed : $1"
        echo "      OS          : ${os:-unknown}"
        echo "      CPU arch    : ${arch:-unknown}"
        echo "      account     : $report_account"
        echo ""
        echo "  No logs, file paths, IP address, hostname, or anything else is collected."
        echo ""
        printf "  Send this report? [y/N] "
    } > /dev/tty

    report_answer=""
    read report_answer < /dev/tty || report_answer=""

    case "$report_answer" in
        y|Y|yes|YES|Yes)
            send_report "$1"
            echo "  Thanks, report sent." > /dev/tty
            ;;
        *)
            echo "  Not sent." > /dev/tty
            ;;
    esac
    echo "" > /dev/tty
}

os=$(uname -s)
arch=$(uname -m)

case "$os" in
    Darwin)
        case "$arch" in
            arm64|aarch64)
                binary="tunnelto-macos-arm64"
                expected_type="Mach-O"
                ;;
            x86_64)
                binary="tunnelto-macos-amd64"
                expected_type="Mach-O"
                ;;
            *)
                echo "error: unsupported macOS architecture: $arch" >&2
                post_report unsupported_arch
                exit 1
                ;;
        esac
        ;;
    Linux)
        case "$arch" in
            x86_64)
                binary="tunnelto-linux-amd64"
                expected_type="ELF"
                ;;
            aarch64|arm64)
                binary="tunnelto-linux-arm64"
                expected_type="ELF"
                ;;
            # 32-bit ARM (Raspberry Pi OS 32-bit, etc). The binary is built
            # for ARMv7 (GOARM=7), so armv6l (Pi Zero/1) is deliberately NOT
            # matched here — it would download and then SIGILL. armv8l is a
            # 32-bit userland on 64-bit ARM hardware.
            armv7l|armv8l|armhf)
                binary="tunnelto-linux-arm"
                expected_type="ELF"
                ;;
            i386|i486|i586|i686|x86)
                binary="tunnelto-linux-386"
                expected_type="ELF"
                ;;
            *)
                echo "error: unsupported Linux architecture: $arch" >&2
                echo "supported: x86_64, aarch64 (arm64), armv7l (arm), i686 (386)" >&2
                post_report unsupported_arch
                exit 1
                ;;
        esac
        ;;
    FreeBSD)
        case "$arch" in
            amd64|x86_64)
                binary="tunnelto-freebsd-amd64"
                expected_type="ELF"
                ;;
            *)
                echo "error: unsupported FreeBSD architecture: $arch" >&2
                echo "supported: amd64" >&2
                post_report unsupported_arch
                exit 1
                ;;
        esac
        ;;
    *)
        cat >&2 <<EOF
error: unsupported operating system: $os

tunnelto supports macOS (Apple Silicon, Intel), Linux (amd64, arm64,
armv7, 386), and FreeBSD (amd64).

For Windows, see:
  https://app.tunnelto.me/getting-started
EOF
        post_report unsupported_os
        exit 1
        ;;
esac

url="$TUNNELTO_SERVER/client/$binary"

echo "Detected: $os $arch"
echo "Installing tunnelto from $url"
echo "Target:   $INSTALL_DIR/tunnelto"
echo ""

if ! mkdir -p "$INSTALL_DIR"; then
    echo "error: could not create install directory $INSTALL_DIR" >&2
    post_report install_dir_failed
    exit 1
fi

# Atomic install: download to a tempfile, sanity-check, then mv into place.
# The trap ensures a failed download leaves no partial file behind.
tmp=$(mktemp "${TMPDIR:-/tmp}/tunnelto.XXXXXX")
trap 'rm -f "$tmp"' EXIT INT TERM

if ! download "$url" "$tmp"; then
    echo "error: download failed from $url" >&2
    post_report download_failed
    exit 1
fi

# Sanity check the binary type matches the current OS. Guards against CDN
# misconfiguration, stale caches, or accidentally-served wrong binaries.
if command -v file >/dev/null 2>&1; then
    detected=$(file -b "$tmp" 2>/dev/null || echo unknown)
    case "$detected" in
        *"$expected_type"*) ;;
        *)
            echo "error: downloaded file is not a $expected_type binary" >&2
            echo "got: $detected" >&2
            echo "please retry, or report at https://tunnelto.me/" >&2
            post_report verify_failed
            exit 1
            ;;
    esac
fi

chmod +x "$tmp"

# macOS hardening: strip xattrs and re-apply an ad-hoc signature.
# Context: on macOS 14.4+ the kernel's AMFI service has started SIGKILL'ing
# ad-hoc-signed Go binaries downloaded via curl, with only "killed" shown in
# the terminal and no log trail. Stripping com.apple.quarantine +
# com.apple.provenance and re-signing locally resets the kernel's cached
# assessment so the binary is allowed to execute. This is a workaround until
# tunnelto is signed+notarized with a real Apple Developer ID.
if [ "$os" = "Darwin" ]; then
    xattr -cr "$tmp" 2>/dev/null || true
    if command -v codesign >/dev/null 2>&1; then
        codesign --force --sign - "$tmp" >/dev/null 2>&1 || true
    fi
fi

mv "$tmp" "$INSTALL_DIR/tunnelto"
trap - EXIT INT TERM

echo "tunnelto installed to $INSTALL_DIR/tunnelto"

# PATH nudge.
case ":${PATH:-}:" in
    *":$INSTALL_DIR:"*) ;;
    *)
        echo ""
        echo "note: $INSTALL_DIR is not in your PATH."
        echo "add to your shell rc (~/.zshrc, ~/.bashrc, etc.):"
        echo "    export PATH=\"\$HOME/.local/bin:\$PATH\""
        ;;
esac

# Auto-run first-run setup when a controlling TTY is available. Under
# `curl | sh` this script's own stdin is the curl pipe (closed), so an
# interactive token prompt would immediately EOF. /dev/tty points at
# the user's actual terminal and stays open as long as one exists —
# redirecting tunnelto's stdin from it lets the prompt work. CI, cron,
# or any no-TTY invocation falls through to a manual "run tunnelto"
# hint. `exec` replaces this shell so tunnelto's exit code propagates.
echo ""

# Forward a pre-filled token to first-run setup via the client's existing
# --token flag. Unquoted on purpose so it splits into two args; access
# tokens are URL-safe (no whitespace). Empty when no token was provided.
setup_args=""
if [ -n "$TUNNELTO_TOKEN" ]; then
    setup_args="--token $TUNNELTO_TOKEN"
fi

# /dev/tty can exist as a device node yet not be openable when there is no
# controlling terminal (some containers, sandboxes, restricted SSH, CI). The
# `[ -c /dev/tty ]` test only checks the node exists, so probe an actual open
# too: otherwise `exec ... </dev/tty` below aborts the whole script with a
# redirection error (exit 2) *after* a successful install, making a working
# install look broken. If the open fails, fall through to the manual hint.
if [ -c /dev/tty ] && (: </dev/tty) 2>/dev/null; then
    if [ -z "$TUNNELTO_TOKEN" ]; then
        # Up-front token-source hint before the interactive prompt fires.
        # Anyone landing here from the marketing site without having logged
        # in yet would otherwise be staring at a bare "Token:" prompt with
        # no context.
        echo "First-run setup will ask for an access token."
        echo "Create or copy a token at: $TUNNELTO_SERVER/tokens"
        echo "(Sign in or sign up first if you don't have an account yet.)"
        echo ""
    fi
    echo "Starting first-run setup..."
    echo ""
    # shellcheck disable=SC2086
    exec "$INSTALL_DIR/tunnelto" $setup_args </dev/tty
fi

# No controlling TTY (CI, container, `curl | sh` with no terminal). With a
# token we can still finish — the client defaults the machine name to the
# hostname when the name prompt reads EOF. Without one, fall back to a hint.
if [ -n "$TUNNELTO_TOKEN" ]; then
    echo "Starting first-run setup..."
    echo ""
    # shellcheck disable=SC2086
    exec "$INSTALL_DIR/tunnelto" $setup_args
fi

echo "next: run"
echo "    tunnelto"
echo "to set up credentials."
echo "(or run $INSTALL_DIR/tunnelto directly if PATH is not updated yet)"
echo ""
echo "Get your access token at: $TUNNELTO_SERVER/tokens"
