#!/bin/bash set -euo pipefail # install.sh — curl | bash installer for sovereign_browser # # Installs: # * apt runtime deps (WebKitGTK 4.1, libsoup-3, tor, build toolchain) # * FIPS prebuilt binary from GitHub releases (source-build fallback via # --build-fips-from-source) with CAP_NET_ADMIN # * sovereign_browser prebuilt binary from Gitea releases (source-build fallback) # * a `sovereign-browser` wrapper that detaches the GUI # # Tor and FIPS are installed by default — they are NOT optional. # # Usage: # curl -fsSL | bash # curl -fsSL | bash -s -- --build-from-source # ./install.sh [options] # --- Config -------------------------------------------------------------- GITEA_API="https://git.laantungir.net/api/v1/repos/laantungir/sovereign_browser" GITEA_DOWNLOAD_BASE="https://git.laantungir.net/laantungir/sovereign_browser/releases/download" SB_REPO="https://git.laantungir.net/laantungir/sovereign_browser.git" FIPS_GITHUB_API="https://api.github.com/repos/jmcorgan/fips/releases/latest" FIPS_REPO="https://github.com/jmcorgan/fips.git" INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}" BIN_NAME="sovereign_browser" # Runtime apt packages (non-dev). git/build-essential/pkg-config are included # because they're needed for the FIPS source build and the source-build fallback. APT_PACKAGES=( libwebkit2gtk-4.1-0 libsoup-3.0-0 libqrencode4 libsqlite3-0 libsecp256k1-2 libssl3t64 libcurl4t64 zlib1g tor git build-essential pkg-config libcap2-bin ) # libclang-dev is only needed when building FIPS from source (bindgen). APT_PACKAGES_FIPS_SOURCE=(libclang-dev) # --- Output helpers ------------------------------------------------------ if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi print_info() { echo -e "${BLUE}[INFO]${NC} $*" >&2; } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $*" >&2; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $*" >&2; } print_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } die() { print_error "$*"; exit 1; } # --- Privilege helper ---------------------------------------------------- # Returns "sudo" if the target path is not user-writable, else "". sudo_for() { local target="$1" if [[ -w "$target" ]]; then echo "" else command -v sudo >/dev/null 2>&1 || die "sudo is required to write to $target but is not available." echo "sudo" fi } # --- Usage --------------------------------------------------------------- show_usage() { cat <<'EOF' sovereign_browser installer Usage: curl -fsSL | bash or: curl -fsSL | bash -s -- --build-from-source or: ./install.sh [options] Options: --build-from-source Build sovereign_browser from source instead of downloading the prebuilt binary --build-fips-from-source Build FIPS from source (cargo) instead of downloading the prebuilt binary. Slower; needs libclang-dev (auto-installed). --prefix Install prefix (default: /usr/local) --yes Skip confirmation prompts -h, --help Show this help EOF } # --- Args ---------------------------------------------------------------- BUILD_FROM_SOURCE=false BUILD_FIPS_FROM_SOURCE=false ASSUME_YES=false while [[ $# -gt 0 ]]; do case "$1" in --build-from-source) BUILD_FROM_SOURCE=true; shift ;; --build-fips-from-source) BUILD_FIPS_FROM_SOURCE=true; shift ;; --prefix) INSTALL_PREFIX="${2:-}"; shift 2 ;; --yes|-y) ASSUME_YES=true; shift ;; -h|--help) show_usage; exit 0 ;; *) die "Unknown option: $1 (try --help)" ;; esac done # --- Platform detection -------------------------------------------------- detect_platform() { print_info "Detecting platform..." local os arch os="$(uname -s)" arch="$(uname -m)" [[ "$os" == "Linux" ]] || die "This installer only supports Linux (got: $os)." if [[ "$arch" != "x86_64" ]]; then if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then die "arm64 is not supported for the prebuilt binary. Re-run with --build-from-source (arm64 source builds are untested)." fi die "Unsupported architecture: $arch (x86_64 only)." fi local distro_id="" version_id="" if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release distro_id="${ID:-}" version_id="${VERSION_ID:-}" fi local distro_ok=false if [[ "$distro_id" == "debian" ]]; then if [[ -n "$version_id" ]] && awk -v v="$version_id" 'BEGIN{exit !(v+0 >= 13)}'; then distro_ok=true fi elif [[ "$distro_id" == "ubuntu" ]]; then if [[ -n "$version_id" ]] && awk -v v="$version_id" 'BEGIN{exit !(v+0 >= 24.04)}'; then distro_ok=true fi fi if $distro_ok; then print_info "Distro: $distro_id $version_id (supported)." else print_warning "Distro '$distro_id $version_id' is not explicitly supported." print_warning "WebKitGTK 4.1 runtime libs are required. If apt cannot find libwebkit2gtk-4.1-0, use Debian 13 (trixie)+ or Ubuntu 24.04+." fi } # --- apt dependencies ---------------------------------------------------- install_deps() { print_info "Installing apt dependencies (runtime libs, tor, build toolchain)..." if ! command -v sudo >/dev/null 2>&1 && [[ $EUID -ne 0 ]]; then die "sudo is required for apt-get but is not available (not running as root)." fi local SUDO="" [[ $EUID -ne 0 ]] && SUDO="sudo" $SUDO apt-get update if ! $SUDO apt-get install -y "${APT_PACKAGES[@]}"; then print_error "apt-get install failed. One or more packages could not be found." print_error "This usually means your distro is too old and lacks WebKitGTK 4.1." print_error "Use Debian 13 (trixie) or Ubuntu 24.04+, then re-run this installer." exit 1 fi print_success "Dependencies installed." } # --- FIPS ---------------------------------------------------------------- # # FIPS is a Rust project (https://github.com/jmcorgan/fips). It publishes # prebuilt release tarballs on GitHub Releases containing the `fips` binary # plus tools (fipsctl, fipstop) and systemd units. We download the prebuilt # x86_64 Linux tarball by default — much faster than a cargo build. # # With --build-fips-from-source, we instead clone the repo and run # `cargo build --release` (requires Rust 1.94.1+ via rustup, and libclang-dev # for the rustables/bindgen nftables bindings). # Download the prebuilt FIPS tarball from GitHub Releases and extract the # `fips` binary. Outputs a stable binary path on stdout (the file is copied # to a caller-managed temp file so it survives after this function returns). # Returns non-zero on failure so the caller can fall back to a source build. download_fips_prebuilt() { print_info "Querying GitHub for latest FIPS release..." local api_json tag asset_url if ! api_json="$(curl -fsSL "$FIPS_GITHUB_API" 2>/dev/null)"; then print_warning "Could not reach FIPS GitHub releases API." return 1 fi tag="$(printf '%s' "$api_json" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')" if [[ -z "$tag" ]]; then print_warning "Could not parse tag_name from FIPS GitHub release JSON." return 1 fi # Find the linux-x86_64 tarball asset URL. asset_url="$(printf '%s' "$api_json" | grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*linux-x86_64\.tar\.gz"' | head -1 | sed -E 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')" if [[ -z "$asset_url" ]]; then print_warning "No linux-x86_64 tarball found in FIPS release $tag." return 1 fi # Use a temp dir for download + extraction. We do NOT use a RETURN trap # here because the caller needs to copy the binary out after this # function returns — a RETURN trap would delete the file first. Instead # we copy the binary to a stable temp file and clean up the extraction # dir ourselves before returning. local tmp fips_bin stable tmp="$(mktemp -d -t fips_dl.XXXXXX)" print_info "Downloading FIPS prebuilt binary: $asset_url" if ! curl -fsSL -o "$tmp/fips.tar.gz" "$asset_url"; then rm -rf "$tmp" print_warning "FIPS tarball download failed." return 1 fi if ! tar -xzf "$tmp/fips.tar.gz" -C "$tmp" 2>/dev/null; then rm -rf "$tmp" print_warning "FIPS tarball extraction failed." return 1 fi # The tarball extracts to fips--linux-x86_64/fips fips_bin="$(find "$tmp" -type f -name fips 2>/dev/null | head -1 || true)" if [[ -z "$fips_bin" ]] || ! file "$fips_bin" 2>/dev/null | grep -qi ELF; then rm -rf "$tmp" print_warning "FIPS tarball did not contain an ELF 'fips' binary." return 1 fi # Copy to a stable path so the caller can access it after we clean up # the extraction dir. mktemp gives a file the caller can rm later. stable="$(mktemp -t fips_binary.XXXXXX)" cp -f "$fips_bin" "$stable" chmod +x "$stable" rm -rf "$tmp" printf '%s' "$stable" } # --- Rust toolchain (only for --build-fips-from-source) ------------------ FIPS_MIN_RUST_MAJOR=1 FIPS_MIN_RUST_MINOR=94 rust_version_ok() { command -v cargo >/dev/null 2>&1 || return 1 local ver ver="$(cargo --version 2>/dev/null | grep -oE 'cargo [0-9]+\.[0-9]+\.' | head -1 | sed 's/cargo //; s/\.$//')" || return 1 [[ -z "$ver" ]] && return 1 local major minor major="${ver%%.*}" minor="${ver#*.}" [[ "$major" -gt "$FIPS_MIN_RUST_MAJOR" ]] && return 0 [[ "$major" -eq "$FIPS_MIN_RUST_MAJOR" && "$minor" -ge "$FIPS_MIN_RUST_MINOR" ]] && return 0 return 1 } ensure_rust() { if rust_version_ok; then print_info "Rust toolchain OK ($(cargo --version))." return 0 fi if command -v cargo >/dev/null 2>&1; then print_warning "Installed cargo ($(cargo --version)) is older than FIPS requires (1.94.1+). Installing a newer toolchain via rustup." else print_info "No cargo found. Installing Rust toolchain via rustup." fi local rustup_init rustup_init="$(mktemp -t rustup-init.XXXXXX)" if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$rustup_init" https://sh.rustup.rs; then rm -f "$rustup_init" die "Failed to download rustup installer." fi if ! sh "$rustup_init" -y --profile minimal >/dev/null 2>&1; then rm -f "$rustup_init" die "rustup installation failed." fi rm -f "$rustup_init" # shellcheck disable=SC1091 . "$HOME/.cargo/env" 2>/dev/null || true export PATH="$HOME/.cargo/bin:$PATH" if ! rust_version_ok; then die "Rust toolchain installed but cargo still reports an insufficient version. FIPS requires 1.94.1+." fi print_success "Rust toolchain installed: $(cargo --version)." } # Build FIPS from source via cargo. Outputs the binary path on stdout. build_fips_from_source() { print_info "Building FIPS from source ($FIPS_REPO)..." ensure_rust local tmp tmp="$(mktemp -d)" trap "rm -rf '$tmp' 2>/dev/null || true" RETURN if ! git clone --depth 1 "$FIPS_REPO" "$tmp/fips"; then die "Failed to clone FIPS repo: $FIPS_REPO" fi local fips_dir="$tmp/fips" print_info "FIPS: running cargo build --release (this can take several minutes)..." if ! ( cd "$fips_dir" && cargo build --release ); then die "cargo build --release failed for FIPS. FIPS is required." fi local fips_bin="" for cand in \ "$fips_dir/target/release/fips" \ "$fips_dir/target/x86_64-unknown-linux-gnu/release/fips"; do if [[ -x "$cand" ]] && file "$cand" 2>/dev/null | grep -qi ELF; then fips_bin="$cand" break fi done if [[ -z "$fips_bin" ]]; then fips_bin="$(find "$fips_dir/target" -type f -name fips -executable 2>/dev/null | head -1 || true)" fi [[ -n "$fips_bin" ]] || die "FIPS build succeeded but no 'fips' binary was found." printf '%s' "$fips_bin" } install_fips() { print_info "Installing FIPS (required, not optional)..." # If source build was requested, install the extra build-time apt deps. if $BUILD_FIPS_FROM_SOURCE; then print_info "Installing FIPS source-build deps (libclang-dev for bindgen)..." local sudo_cmd="" if [[ $EUID -ne 0 ]]; then sudo_cmd="sudo"; fi $sudo_cmd apt-get install -y "${APT_PACKAGES_FIPS_SOURCE[@]}" >/dev/null 2>&1 || true fi local fips_bin="" if $BUILD_FIPS_FROM_SOURCE; then fips_bin="$(build_fips_from_source)" else if ! fips_bin="$(download_fips_prebuilt)"; then print_warning "Prebuilt FIPS unavailable — falling back to source build." # Install source-build deps on the fallback path too. local sudo_cmd="" if [[ $EUID -ne 0 ]]; then sudo_cmd="sudo"; fi $sudo_cmd apt-get install -y "${APT_PACKAGES_FIPS_SOURCE[@]}" >/dev/null 2>&1 || true fips_bin="$(build_fips_from_source)" fi fi [[ -n "$fips_bin" && -x "$fips_bin" ]] || die "FIPS binary not available." local prefix_bin="$INSTALL_PREFIX/bin" local SUDO SUDO="$(sudo_for "$INSTALL_PREFIX")" $SUDO mkdir -p "$prefix_bin" $SUDO cp -f "$fips_bin" "$prefix_bin/fips" $SUDO chmod +x "$prefix_bin/fips" print_success "FIPS binary installed to $prefix_bin/fips" # Clean up the temp binary (download_fips_prebuilt copies to a stable # mktemp file; build_fips_from_source uses a RETURN-trapped dir so its # output is already gone, but rm -f is safe on either). rm -f "$fips_bin" 2>/dev/null || true # setcap for CAP_NET_ADMIN (needed to create a TUN interface in managed mode). local setcap_cmd="" if command -v setcap >/dev/null 2>&1; then setcap_cmd="setcap" elif command -v sudo >/dev/null 2>&1 && $SUDO command -v setcap >/dev/null 2>&1; then setcap_cmd="$SUDO setcap" fi if [[ -n "$setcap_cmd" ]]; then if $setcap_cmd cap_net_admin+ep "$prefix_bin/fips" 2>/dev/null; then print_success "FIPS granted CAP_NET_ADMIN." else if $SUDO setcap cap_net_admin+ep "$prefix_bin/fips" 2>/dev/null; then print_success "FIPS granted CAP_NET_ADMIN." else print_warning "setcap failed. FIPS managed mode (TUN) won't work without CAP_NET_ADMIN." print_warning "Install 'libcap2-bin' and re-run, or run a system FIPS instance and set fips_mode=attach." fi fi else print_warning "'setcap' not found (libcap2-bin not installed)." print_warning "FIPS managed mode won't work without CAP_NET_ADMIN." print_warning "Install libcap2-bin (apt-get install -y libcap2-bin) and re-run, or run a system FIPS instance and set fips_mode=attach." fi } # --- Prebuilt binary download ------------------------------------------- download_binary() { # Outputs the downloaded binary path on stdout. Returns non-zero on failure # so the caller can fall back to a source build. print_info "Querying Gitea for latest release..." local api_json tag url out if ! api_json="$(curl -fsSL "$GITEA_API/releases/latest" 2>/dev/null)"; then print_warning "Could not reach Gitea releases API (no release yet, or network issue)." return 1 fi tag="$(printf '%s' "$api_json" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')" if [[ -z "$tag" ]]; then print_warning "Could not parse tag_name from Gitea release JSON." return 1 fi url="$GITEA_DOWNLOAD_BASE/$tag/$BIN_NAME" out="$(mktemp -t sovereign_browser_download.XXXXXX)" print_info "Downloading prebuilt binary: $url" if ! curl -fsSL -o "$out" "$url"; then rm -f "$out" 2>/dev/null || true print_warning "Download failed for $url" return 1 fi if ! file "$out" 2>/dev/null | grep -qi ELF; then rm -f "$out" 2>/dev/null || true print_warning "Downloaded file is not an ELF binary." return 1 fi printf '%s\n' "$out" print_success "Downloaded sovereign_browser $tag." } # --- Source build fallback ----------------------------------------------- build_from_source() { print_info "Building sovereign_browser from source ($SB_REPO)..." local tmp tmp="$(mktemp -d)" trap "rm -rf '$tmp' 2>/dev/null || true" RETURN if ! git clone --depth 1 "$SB_REPO" "$tmp/sovereign_browser"; then die "Failed to clone sovereign_browser repo: $SB_REPO" fi if ! ( cd "$tmp/sovereign_browser" && make ); then die "Source build failed (make). Ensure build deps are installed." fi [[ -x "$tmp/sovereign_browser/$BIN_NAME" ]] || die "Build finished but ./$BIN_NAME was not produced." local out out="$(mktemp -t sovereign_browser_built.XXXXXX)" cp -f "$tmp/sovereign_browser/$BIN_NAME" "$out" chmod +x "$out" printf '%s\n' "$out" print_success "Built sovereign_browser from source." } # --- Install binary + wrapper ------------------------------------------- install_binary() { local src="$1" local prefix_bin="$INSTALL_PREFIX/bin" local SUDO SUDO="$(sudo_for "$INSTALL_PREFIX")" print_info "Installing binary to $prefix_bin/$BIN_NAME" $SUDO mkdir -p "$prefix_bin" $SUDO cp -f "$src" "$prefix_bin/$BIN_NAME" $SUDO chmod +x "$prefix_bin/$BIN_NAME" # Write the kebab-case wrapper that detaches the GUI. local wrapper="$prefix_bin/sovereign-browser" local real_bin="$prefix_bin/$BIN_NAME" print_info "Installing wrapper to $wrapper" # Write to a temp file then move with sudo if needed. local wtmp wtmp="$(mktemp -t sovereign-browser-wrapper.XXXXXX)" cat > "$wtmp" </dev/null)" 2>/dev/null; then echo "sovereign_browser already running (PID \$(cat "\$SB_PID_FILE"))" >&2 return 0 fi nohup "\$SB_BIN" "\$@" > "\$SB_LOG_FILE" 2>&1 & local pid=\$! echo "\$pid" > "\$SB_PID_FILE" disown "\$pid" 2>/dev/null || true echo "sovereign_browser started (PID \$pid)" >&2 } sb_stop() { if [[ ! -f "\$SB_PID_FILE" ]]; then echo "sovereign_browser is not running (no PID file)" >&2 return 0 fi local pid pid="\$(cat "\$SB_PID_FILE" 2>/dev/null || true)" if [[ -n "\$pid" ]] && kill -0 "\$pid" 2>/dev/null; then kill "\$pid" 2>/dev/null || true echo "sovereign_browser stopped (PID \$pid)" >&2 else echo "sovereign_browser was not running (stale PID file)" >&2 fi rm -f "\$SB_PID_FILE" 2>/dev/null || true } sb_status() { if [[ -f "\$SB_PID_FILE" ]] && kill -0 "\$(cat "\$SB_PID_FILE" 2>/dev/null)" 2>/dev/null; then echo "sovereign_browser is running (PID \$(cat "\$SB_PID_FILE"))" >&2 return 0 else echo "sovereign_browser is not running" >&2 return 1 fi } sb_log() { tail -n 50 "\$SB_LOG_FILE" 2>/dev/null || echo "(no log file yet)" >&2 } case "\${1:-start}" in start) shift 2>/dev/null || true; sb_start "\$@" ;; stop) sb_stop ;; restart) sb_stop; shift 2>/dev/null || true; sb_start "\$@" ;; status) sb_status ;; log) sb_log ;; *) sb_start "\$@" ;; # default: treat all args as browser args esac WRAPPER_EOF $SUDO mv -f "$wtmp" "$wrapper" $SUDO chmod +x "$wrapper" print_success "Wrapper installed: $wrapper" if [[ ":${PATH}:" != *":$prefix_bin:"* ]]; then print_warning "$prefix_bin is not in your PATH. Add it: export PATH=\"$prefix_bin:\$PATH\"" fi } # --- Main ---------------------------------------------------------------- main() { detect_platform # Summary + confirmation local method="prebuilt binary from Gitea" $BUILD_FROM_SOURCE && method="source build from $SB_REPO" local fips_method="prebuilt binary from GitHub releases" $BUILD_FIPS_FROM_SOURCE && fips_method="source build ($FIPS_REPO, cargo)" # Use print_info so colors render via `echo -e` (a plain `cat` heredoc # would print the literal \033 escape sequences). print_info "sovereign_browser will be installed:" print_info " Install prefix : $INSTALL_PREFIX" print_info " Binary method : $method" print_info " FIPS : $fips_method, CAP_NET_ADMIN set" print_info " Tor : apt package 'tor'" print_info " apt deps : ${APT_PACKAGES[*]}" if ! $ASSUME_YES; then print_warning "This will run apt-get install (sudo) and clone/build FIPS. Continue? [y/N]" # Read from /dev/tty, not stdin: in `curl | bash` mode stdin is the # curl pipe (the script text), so a plain `read` would hit EOF and # abort immediately. /dev/tty is the user's terminal. if [[ -r /dev/tty ]]; then read -r reply < /dev/tty || reply="" else # No tty available (e.g. non-interactive CI) — require --yes. die "No tty available for confirmation. Re-run with --yes to skip prompts." fi reply="${reply:-n}" if [[ ! "${reply,,}" =~ ^y(es)?$ ]]; then die "Aborted by user." fi fi install_deps install_fips local bin_path="" if $BUILD_FROM_SOURCE; then bin_path="$(build_from_source)" else if ! bin_path="$(download_binary)"; then print_warning "Prebuilt binary unavailable — falling back to source build." bin_path="$(build_from_source)" fi fi install_binary "$bin_path" rm -f "$bin_path" 2>/dev/null || true print_success "sovereign_browser installed." cat >&2 <