#!/bin/sh # Public Linux installer. No credentials, package installs, or daemon startup. # Each release packages Caterm and its checksum-pinned native Chrome controller. set -eu fail() { printf 'Caterm installer: %s\n' "$*" >&2 exit 1 } download() { curl --proto '=https' --proto-redir '=https' --tlsv1.2 \ --fail --silent --show-error --location --retry 2 \ --connect-timeout 15 --max-time 300 --output "$2" "$1" || fail "Download failed: $1. Your existing installation has been kept." } valid_version() { printf '%s\n' "$1" | awk ' /^[0-9]+\.[0-9]+\.[0-9]+$/ { valid++ } END { exit !(NR == 1 && valid == 1) }' } main() { version=latest bin_dir= while [ "$#" -gt 0 ]; do case "$1" in --version) [ "$#" -ge 2 ] || fail '--version needs X.Y.Z' version=${2#v} shift 2 ;; --bin-dir) [ "$#" -ge 2 ] || fail '--bin-dir needs an absolute directory' bin_dir=$2 shift 2 ;; -h|--help) printf '%s\n' \ 'Usage: sh install.sh [--version X.Y.Z] [--bin-dir /absolute/path]' \ 'Linux x86_64 / ARM64, glibc 2.36+. Default: $HOME/.local/bin.' \ 'Rerun to upgrade. Stop and restart the daemon yourself.' return ;; *) fail "Unknown option: $1" ;; esac done [ "$(uname -s)" = Linux ] || fail 'This installer is for Linux. Use the Mac app on macOS.' case "$(uname -m)" in x86_64|amd64) arch=x86_64 ;; aarch64|arm64) arch=aarch64 ;; *) fail "Unsupported architecture: $(uname -m)" ;; esac for tool in curl tar awk sed mktemp chmod mv cp rm readlink getconf; do command -v "$tool" >/dev/null 2>&1 || fail "Missing $tool; install it with your distribution's package manager." done libc=$(getconf GNU_LIBC_VERSION 2>/dev/null) || fail 'A glibc Linux distribution is required; Alpine/musl is not supported.' printf '%s\n' "$libc" | awk ' $1 == "glibc" { split($2, v, "."); ok = v[1] > 2 || (v[1] == 2 && v[2] >= 36) } END { exit !ok }' || fail "glibc 2.36 or newer is required; found $libc." if command -v sha256sum >/dev/null 2>&1; then checksum=sha256sum elif command -v shasum >/dev/null 2>&1; then checksum=shasum else fail 'sha256sum or shasum is required to verify the binary.' fi if [ -z "$bin_dir" ]; then [ -n "${HOME:-}" ] || fail 'HOME is unset; use --bin-dir.' bin_dir=$HOME/.local/bin fi case "$bin_dir" in /*) ;; *) fail '--bin-dir must be an absolute path' ;; esac # Only the old managed alias can be replaced by a regular executable. if [ -L "$bin_dir/caterm" ]; then [ "$(readlink "$bin_dir/caterm")" = catermd ] || fail "$bin_dir/caterm is an unrelated symlink; choose another --bin-dir." elif [ -e "$bin_dir/caterm" ] && [ ! -f "$bin_dir/caterm" ]; then fail "$bin_dir/caterm is not a regular executable; choose another --bin-dir." fi remove_legacy=false if [ -e "$bin_dir/catermd" ] || [ -L "$bin_dir/catermd" ]; then if [ -L "$bin_dir/catermd" ] && [ "$(readlink "$bin_dir/catermd")" = caterm ]; then remove_legacy=true elif [ -f "$bin_dir/catermd" ] && [ ! -L "$bin_dir/catermd" ]; then legacy_identity=$("$bin_dir/catermd" --version 2>/dev/null) || fail 'Cannot identify the old catermd; remove it manually or choose another --bin-dir.' case "$legacy_identity" in "catermd "*" (protocol "*) remove_legacy=true ;; *) fail 'The old catermd path is unrelated to Caterm; choose another --bin-dir.' ;; esac else fail 'The old catermd path is not a Caterm executable; choose another --bin-dir.' fi fi if [ "$version" != latest ]; then valid_version "$version" || fail '--version must be X.Y.Z' fi umask 077 temporary=$(mktemp -d "${TMPDIR:-/tmp}/caterm-install.XXXXXXXX") stage= replacing=false committed=false cleanup() { if [ "$replacing" = true ] && [ "$committed" = false ]; then for name in caterm caterm-agent-browser caterm-agent-browser-LICENSE; do if [ -e "$stage/previous/$name" ] || [ -L "$stage/previous/$name" ]; then mv -fT "$stage/previous/$name" "$bin_dir/$name" else rm -f "$bin_dir/$name" fi done fi rm -rf "$temporary" if [ -n "$stage" ]; then rm -rf "$stage"; fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM HUP base=https://downloads.caterm.com if [ "$version" = latest ]; then download "$base/channels/stable.json" "$temporary/stable.json" # Read only the publisher's numeric version field. Never evaluate a manifest. version=$(sed -n 's/^ "version": "\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)",$/\1/p' "$temporary/stable.json") valid_version "$version" || fail 'Invalid stable release manifest.' fi package=caterm-linux-$arch archive=$package.tar.gz printf 'Installing Caterm %s for Linux %s into %s\n' "$version" "$arch" "$bin_dir" download "$base/releases/$version/$archive" "$temporary/$archive" download "$base/releases/$version/$archive.sha256" "$temporary/$archive.sha256" expected=$(awk -v name="$archive" ' NF == 2 && $2 == name && length($1) == 64 && $1 !~ /[^0-9a-f]/ { digest = $1; valid++ } END { if (NR == 1 && valid == 1) print digest; else exit 1 }' "$temporary/$archive.sha256") || fail 'Invalid SHA-256 file.' if [ "$checksum" = sha256sum ]; then actual=$(sha256sum "$temporary/$archive" | awk '{print $1}') else actual=$(shasum -a 256 "$temporary/$archive" | awk '{print $1}') fi [ "$actual" = "$expected" ] || fail 'SHA-256 mismatch. Your existing installation has been kept.' mkdir -p "$bin_dir" stage=$(mktemp -d "$bin_dir/.caterm-install.XXXXXXXX") # Extract only the expected executable as bytes, never archive paths/symlinks. tar -xzOf "$temporary/$archive" "$package/caterm" > "$stage/caterm" || fail 'Could not extract the daemon.' [ -s "$stage/caterm" ] || fail 'The downloaded executable is empty.' chmod 755 "$stage/caterm" identity=$("$stage/caterm" --version) || fail 'The downloaded binary cannot run on this machine. Your existing installation has been kept.' case "$identity" in "caterm $version (protocol "*) ;; *) fail "Unexpected binary identity: $identity" ;; esac # Chrome Use attaches to the user's Chrome; this tool is not a browser engine. if [ -L "$bin_dir/caterm-agent-browser" ] || { [ -e "$bin_dir/caterm-agent-browser" ] && [ ! -f "$bin_dir/caterm-agent-browser" ]; }; then fail "$bin_dir/caterm-agent-browser is not a regular executable; choose another --bin-dir." fi tar -xzOf "$temporary/$archive" "$package/caterm-agent-browser" > "$stage/agent-browser" || fail 'Could not extract the Chrome controller.' [ -s "$stage/agent-browser" ] || fail 'The Chrome controller is empty.' chmod 755 "$stage/agent-browser" "$stage/agent-browser" --version >/dev/null || fail 'The Chrome controller cannot run on this machine.' tar -xzOf "$temporary/$archive" "$package/caterm-agent-browser-LICENSE" > "$stage/agent-browser-LICENSE" || fail 'Could not extract the Chrome controller license.' [ -s "$stage/agent-browser-LICENSE" ] || fail 'The Chrome controller license is empty.' if [ -L "$bin_dir/caterm-agent-browser-LICENSE" ] || { [ -e "$bin_dir/caterm-agent-browser-LICENSE" ] && [ ! -f "$bin_dir/caterm-agent-browser-LICENSE" ]; }; then fail 'Chrome controller license destination is not a regular file.' fi mkdir "$stage/previous" for name in caterm caterm-agent-browser caterm-agent-browser-LICENSE; do if [ -e "$bin_dir/$name" ] || [ -L "$bin_dir/$name" ]; then cp -P "$bin_dir/$name" "$stage/previous/$name" fi done replacing=true mv -fT "$stage/agent-browser" "$bin_dir/caterm-agent-browser" mv -fT "$stage/agent-browser-LICENSE" "$bin_dir/caterm-agent-browser-LICENSE" # GNU mv -T replaces a destination, never moves into a directory/symlink. mv -fT "$stage/caterm" "$bin_dir/caterm" committed=true # Retire the old command only after the verified replacement is installed. if [ "$remove_legacy" = true ]; then rm -f "$bin_dir/catermd" fi printf '%s\n' "$identity" 'Installed caterm. No services were started.' case ":${PATH:-}:" in *":$bin_dir:"*) ;; *) printf 'Add this directory to PATH: %s\n' "$bin_dir" if [ "$bin_dir" = "${HOME:-}/.local/bin" ]; then printf '%s\n' 'For this shell: export PATH="$HOME/.local/bin:$PATH"' fi ;; esac printf '%s\n' \ 'Use Add a device on caterm.com, then run its connection command.' \ 'For desktop sharing: caterm desktop doctor, then caterm connect --desktop --saved.' \ 'Desktop prerequisites and applications must be installed by you.' \ 'For Chrome: caterm plugins install chrome-use (or gmail-use), then restart Caterm.' \ 'In Chrome 144+, enable chrome://inspect/#remote-debugging and allow the connection.' } # Defining the whole installer before invoking it also avoids executing a # partially downloaded function body when this script is piped into sh. main "$@"