#!/bin/sh # tofa native installer — https://get.tofa.tv # # curl -fsSL https://get.tofa.tv | sh # # Downloads the latest beta build for your OS/arch, installs it as a background # service, and points you at http://localhost:33333 to claim the server to your # tofa account. Supports Linux (x86_64 / arm64, systemd) and macOS (Apple # Silicon). For Docker, Windows, or Intel Macs, see: # https://docs.tofa.tv/run-your-server.html # # This is readable on purpose — you're piping it to a shell, so read it first. set -eu DIST_BASE="https://github.com/tofatv/tofa-dist/releases/download/beta" PORT=33333 say() { printf '\033[1;36m==>\033[0m %s\n' "$1"; } warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$1" >&2; } die() { printf '\033[1;31merror:\033[0m %s\n' "$1" >&2; exit 1; } # A setup token (SETUP_ACCESS_TOKEN) lets you claim the server from ANY device # via a tokened setup URL; without it the server only accepts a claim from its # own local network. We wire it in for headless (systemd) installs, which are # the ones most often reached from another machine. gen_token() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 24 else LC_ALL=C od -An -tx1 -N24 /dev/urandom | tr -d ' \n' fi } # Every non-loopback IPv4 address on the box (all interfaces), most-likely-LAN # first, deduped — so the claim output can offer a real URL for each instead of # a fake placeholder. Container/virtual bridges are skipped (not reachable off # the box). Empty output is fine; the caller falls back to the SSH tip. list_ipv4s() { { if command -v ip >/dev/null 2>&1; then ip -4 -o addr show scope global 2>/dev/null \ | awk '$2 !~ /^(docker|br-|veth|virbr|tofa|cni|flannel|kube|zt|tailscale)/ {print $4}' \ | cut -d/ -f1 elif command -v hostname >/dev/null 2>&1; then hostname -I 2>/dev/null | tr ' ' '\n' fi } 2>/dev/null \ | grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$' \ | grep -vE '^(127\.|169\.254\.|172\.17\.|0\.0\.0\.0)' \ | awk '!seen[$0]++' } # Print every way to reach the claim page. $1 = setup token (may be empty). print_claim() { _q="" [ -n "${1:-}" ] && _q="?setup_token=$1" _ssh_user="${SUDO_USER:-$(id -un 2>/dev/null || echo user)}" _ssh_host="$(hostname 2>/dev/null || echo this-server)" printf '\n\033[1;32mtofa is running.\033[0m Sign in to claim it to your account:\n' printf ' • On this machine: \033[1;36mhttp://localhost:%s/setup%s\033[0m\n' "$PORT" "$_q" _ips="$(list_ipv4s)" if [ -n "$_ips" ]; then printf ' • From another device on your network (try each until one loads):\n' printf '%s\n' "$_ips" | while IFS= read -r _ip; do [ -n "$_ip" ] && printf ' \033[1;36mhttp://%s:%s/setup%s\033[0m\n' "$_ip" "$PORT" "$_q" done fi printf ' • Headless / over SSH — forward the port from your own computer, then open the localhost URL there:\n' printf ' \033[1;36mssh -L %s:localhost:%s %s@%s\033[0m\n' "$PORT" "$PORT" "$_ssh_user" "$_ssh_host" firewall_hint "$PORT" if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "http://localhost:${PORT}/setup${_q}" >/dev/null 2>&1 || true fi } # If a host firewall is active it will block reaching tofa from other devices — # common on RHEL/Fedora (firewalld on by default) and Ubuntu (ufw). Detect it # read-only (no root needed) and print the exact command to open the port. firewall_hint() { _p="$1" if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then warn "firewalld is active — to reach tofa from other devices, open port ${_p}:" printf ' \033[1;36msudo firewall-cmd --permanent --add-port=%s/tcp && sudo firewall-cmd --reload\033[0m\n' "$_p" >&2 elif command -v ufw >/dev/null 2>&1 && grep -qi '^ENABLED=yes' /etc/ufw/ufw.conf 2>/dev/null; then warn "ufw is active — to reach tofa from other devices, open port ${_p}:" printf ' \033[1;36msudo ufw allow %s/tcp\033[0m\n' "$_p" >&2 fi } # --- privilege helper (installer touches /opt, systemd, /Applications) ------- SUDO="" if [ "$(id -u)" -ne 0 ]; then if command -v sudo >/dev/null 2>&1; then SUDO="sudo" else die "need root to install a system service — re-run as root, or install sudo." fi fi command -v curl >/dev/null 2>&1 || die "curl is required to download tofa." command -v tar >/dev/null 2>&1 || die "tar is required to unpack tofa." # --- detect platform --------------------------------------------------------- OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) case "$ARCH" in x86_64|amd64) PLATFORM="linux-x64" ;; aarch64|arm64) PLATFORM="linux-arm64" ;; *) die "unsupported Linux arch '$ARCH' (need x86_64 or aarch64)." ;; esac command -v systemctl >/dev/null 2>&1 || \ die "this installer targets systemd distros. For others, see https://docs.tofa.tv/run-your-server.html" ;; Darwin) case "$ARCH" in arm64) PLATFORM="macos-arm64" ;; *) die "Intel Macs aren't supported natively yet — use Docker: https://docs.tofa.tv/run-your-server.html" ;; esac ;; *) die "unsupported OS '$OS'. See https://docs.tofa.tv/run-your-server.html" ;; esac TARBALL="tofa-beta-${PLATFORM}.tar.gz" URL="${DIST_BASE}/${TARBALL}" say "Installing tofa (beta) for ${PLATFORM}" # --- download + verify checksum ---------------------------------------------- TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT cd "$TMP" say "Downloading ${TARBALL}" curl -fsSL "$URL" -o "$TARBALL" || die "download failed: $URL" curl -fsSL "${URL}.sha256" -o "sum" || die "checksum download failed." say "Verifying checksum" EXPECTED="$(awk '{print $1}' sum)" [ -n "$EXPECTED" ] || die "empty checksum file — refusing to install." if command -v sha256sum >/dev/null 2>&1; then ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then ACTUAL="$(shasum -a 256 "$TARBALL" | awk '{print $1}')" else die "no sha256 tool (sha256sum/shasum) — cannot verify download." fi [ "$EXPECTED" = "$ACTUAL" ] || die "checksum mismatch — refusing to install." tar xzf "$TARBALL" || die "failed to unpack $TARBALL." SRC="tofa-beta-${PLATFORM}" [ -d "$SRC" ] || die "unexpected archive layout (missing $SRC/)." # --- macOS: hand off to the bundled installer, which owns the whole macOS ----- # experience (app install, launchd service, health wait, claim output). if [ "$OS" = "Darwin" ]; then [ -f "${SRC}/install.sh" ] || die "bundled macOS installer missing from the archive." say "Installing tofa.app + launchd service (you may be asked for your password)" $SUDO sh "${SRC}/install.sh" exit 0 fi # --- Linux: install to /opt/tofa + systemd ----------------------------------- say "Installing to /opt/tofa + systemd service" # Stop any running instance first. A running executable can't be overwritten # with cp (ETXTBSY), which would otherwise abort an in-place upgrade under set -e. $SUDO systemctl stop tofa.service 2>/dev/null || true if ! getent passwd tofa >/dev/null 2>&1; then $SUDO useradd --system --user-group --shell /usr/sbin/nologin \ --home-dir /home/tofa --create-home tofa \ || die "failed to create the 'tofa' system user." fi $SUDO mkdir -p /opt/tofa/lib /home/tofa/.tofa /etc/tofa $SUDO cp "${SRC}/tofa" /opt/tofa/tofa [ -f "${SRC}/tofa-updater" ] && $SUDO cp "${SRC}/tofa-updater" /opt/tofa/tofa-updater [ -f "${SRC}/package_info" ] && $SUDO cp "${SRC}/package_info" /opt/tofa/package_info # Copy lib/ contents one file at a time so an (unexpected) empty dir doesn't trip # `set -e` on an unexpanded glob. if [ -d "${SRC}/lib" ]; then for f in "${SRC}/lib"/*; do [ -e "$f" ] && $SUDO cp "$f" /opt/tofa/lib/ done fi $SUDO chmod 755 /opt/tofa/tofa [ -f /etc/tofa/tofa.env ] || printf '# tofa environment overrides (read by tofa.service)\n' | $SUDO tee /etc/tofa/tofa.env >/dev/null # Setup token for claiming from another device. Reuse an existing one so a # re-run keeps the same claim URL; otherwise mint and append it. if $SUDO grep -q '^SETUP_ACCESS_TOKEN=' /etc/tofa/tofa.env 2>/dev/null; then TOKEN="$($SUDO sh -c "grep '^SETUP_ACCESS_TOKEN=' /etc/tofa/tofa.env | head -1 | cut -d= -f2-")" else TOKEN="$(gen_token)" printf 'SETUP_ACCESS_TOKEN=%s\n' "$TOKEN" | $SUDO tee -a /etc/tofa/tofa.env >/dev/null fi $SUDO cp "${SRC}/systemd/tofa.service" /etc/systemd/system/tofa.service $SUDO chown -R tofa:tofa /opt/tofa /home/tofa/.tofa say "Starting the tofa service" $SUDO systemctl daemon-reload $SUDO systemctl enable tofa.service >/dev/null 2>&1 || true $SUDO systemctl restart tofa.service \ || die "tofa.service failed to start — check: sudo journalctl -u tofa -n 50" # --- wait for health, then point at the claim page --------------------------- say "Waiting for tofa to come up (first run downloads its database, ~1 min)…" i=0 until curl -fsS "http://localhost:${PORT}/api/v1/health" >/dev/null 2>&1; do i=$((i + 1)) if [ "$i" -gt 180 ]; then warn "tofa is taking longer than usual to answer on :${PORT}; it may still be starting." warn "If it doesn't come up, check: sudo journalctl -u tofa -n 50" break fi sleep 1 done # Print the claim details regardless — the service stays enabled and keeps # starting, so a slow first boot shouldn't leave the user without their URL. print_claim "${TOKEN:-}"