#!/usr/bin/env bash # install-ipatool.sh — Install ipatool from the latest GitHub release. # Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification # from the release checksum file, atomic install to /usr/local/bin, --dry-run. set -euo pipefail IFS=$'\n\t' readonly SCRIPT_NAME="${0##*/}" DRY_RUN=0 REPO="majd/ipatool" INSTALL_PATH="/usr/local/bin/ipatool" usage() { cat <&2; } die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR while (( $# )); do case "$1" in --dry-run) DRY_RUN=1 ;; -h|--help) usage; exit 0 ;; *) die "Unknown argument: $1 (try --help)" ;; esac shift done (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" [[ -r /etc/os-release ]] || die "/etc/os-release not found." # shellcheck disable=SC1091 . /etc/os-release log "Detected: ${PRETTY_NAME:-unknown}" ARCH_RAW="$(uname -m)" case "$ARCH_RAW" in x86_64) GO_ARCH=amd64 ;; aarch64) GO_ARCH=arm64 ;; armv7l|armv6l) GO_ARCH=arm ;; *) die "Unsupported architecture: $ARCH_RAW" ;; esac ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz" export DEBIAN_FRONTEND=noninteractive log "Installing prerequisites..." run "apt-get update -qq" run "apt-get install -y curl jq tar ca-certificates libsecret-1-0" GH_HDRS=(-H "Accept: application/vnd.github+json") [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") log "Querying GitHub API for latest release of $REPO..." RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." log "Latest release: $TAG" DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'." STAGE="$(mktemp -d -t ipatool.XXXXXX)" trap 'rm -rf "$STAGE"' EXIT TARBALL="$STAGE/ipatool.tar.gz" log "Downloading $(basename "$DOWNLOAD_URL")..." run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'" if [[ -n "$CHECKSUM_URL" ]]; then log "Verifying SHA-256..." EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')" ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL" fi [[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping." else warn "No checksums.txt in release; skipping SHA-256 verification." fi log "Extracting..." run "tar -xzf '$TARBALL' -C '$STAGE'" BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)" if [[ -z "$BINARY_PATH" ]]; then # Some releases ship the binary without +x; relax the find BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)" fi [[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive." log "Installing to $INSTALL_PATH..." run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'" if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)" fi log "Done."