#!/usr/bin/env bash set -Eeuo pipefail umask 077 readonly app_name="AiTECH Manager" readonly app_version="1.0.0" readonly install_root="/opt/aitech-manager" readonly build_dir="${install_root}/build" readonly bin_dir="${install_root}/bin" readonly config_dir="${install_root}/config" readonly log_dir="${install_root}/logs" readonly temp_dir="${install_root}/tmp" readonly system_config_dir="/etc/aitech-manager" readonly system_certificate_dir="${system_config_dir}/certificates" readonly system_log_dir="/var/log/aitech-manager" readonly system_temp_dir="/var/tmp/aitech-manager" readonly core_binary="${build_dir}/aitech-core" readonly launcher="${bin_dir}/aitech" readonly system_launcher="/usr/local/bin/aitech" readonly manager_config="${config_dir}/manager.conf" readonly server_public_key="${config_dir}/server_public.pem" readonly verify_service="/etc/systemd/system/aitech-manager-verify.service" readonly verify_timer="/etc/systemd/system/aitech-manager-verify.timer" readonly verify_timer_name="aitech-manager-verify.timer" readonly release_repository="Alouk0/aitech-manager-releases" readonly release_tag="v1.0.0" readonly release_binary_name="aitech-core-linux-amd64" readonly release_binary_url="https://github.com/${release_repository}/releases/download/${release_tag}/${release_binary_name}" readonly release_checksum_url="https://github.com/${release_repository}/releases/download/${release_tag}/${release_binary_name}.sha256" readonly release_public_key_url="https://github.com/${release_repository}/releases/download/${release_tag}/server_public.pem" readonly expected_binary_sha256="ab629817e75332a7ec89cb45836a9a2c4d2dc6cb056478984b7b6912d25336ce" readonly expected_public_key_sha256="02213a4a045dd8629e372115fa08861df123d491cff9922398f3e157b8bea06d" readonly license_server_url="https://install.fastlysmarty.co.uk/api" readonly backup_base="/opt/aitech-manager-backups/client-installer" license_key="${AITECH_LICENSE_KEY:-}" check_only=0 temporary_directory="" backup_directory="" installation_started=0 installation_completed=0 previous_timer_enabled=0 previous_timer_active=0 usage() { cat <<'USAGE' AiTECH Manager client installer Usage: sudo bash install.sh sudo bash install.sh --license-key sudo bash install.sh --check sudo bash install.sh --help One-command installation: curl -fsSL https://install.fastlysmarty.co.uk/ | sudo bash -s -- The installer does not print the supplied license key. USAGE } log() { printf '[AiTECH] %s\n' "$*" } fail() { printf '[AiTECH] ERROR: %s\n' "$*" >&2 return 1 } parse_arguments() { while [ "$#" -gt 0 ]; do case "$1" in --check) check_only=1 ;; --help|-h) usage exit 0 ;; --license-key) shift [ "$#" -gt 0 ] || fail "--license-key requires a value" license_key="$1" ;; --license-key=*) license_key="${1#*=}" ;; --*) fail "unknown option: $1" ;; *) if [ -n "$license_key" ]; then fail "multiple license keys were supplied" fi license_key="$1" ;; esac shift done license_key="$(printf '%s' "$license_key" | tr -d '\r\n')" if [ "$check_only" -eq 0 ] && [ -z "$license_key" ]; then usage >&2 fail "a client license key is required" fi } require_root() { if [ "$(id -u)" -ne 0 ]; then fail "run this installer as root or with sudo" fi } detect_platform() { [ -r /etc/os-release ] || fail "/etc/os-release is unavailable" local os_id="" local version_id="" # shellcheck disable=SC1091 . /etc/os-release os_id="${ID:-}" version_id="${VERSION_ID:-}" case "${os_id}:${version_id}" in ubuntu:20.04|ubuntu:22.04|ubuntu:24.04|debian:11|debian:12|debian:13) ;; *) fail "unsupported operating system: ${os_id:-unknown} ${version_id:-unknown}" ;; esac case "$(uname -m)" in x86_64|amd64) ;; *) fail "unsupported architecture: $(uname -m); this release requires amd64" ;; esac [ -d /run/systemd/system ] || fail "systemd is required on the client VPS" log "Supported platform detected: ${os_id} ${version_id} amd64" } ensure_dependencies() { local missing=() local command="" for command in curl sha256sum install mktemp systemctl systemd-analyze awk tr cp rm ln chmod mkdir date; do if ! command -v "$command" >/dev/null 2>&1; then missing+=("$command") fi done if [ "${#missing[@]}" -eq 0 ]; then return 0 fi command -v apt-get >/dev/null 2>&1 || fail "required commands are missing and apt-get is unavailable: ${missing[*]}" log "Installing required system packages" export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends ca-certificates curl coreutils systemd for command in curl sha256sum install mktemp systemctl systemd-analyze awk tr cp rm ln chmod mkdir date; do command -v "$command" >/dev/null 2>&1 || fail "required command is still unavailable after package installation: $command" done } create_temporary_directory() { temporary_directory="$(mktemp -d /tmp/aitech-manager-install.XXXXXX)" } download_file() { local url="$1" local destination="$2" curl \ --fail \ --silent \ --show-error \ --location \ --proto '=https' \ --tlsv1.2 \ --retry 5 \ --retry-delay 2 \ --connect-timeout 15 \ --max-time 300 \ --user-agent "AiTECH-Manager-Installer/${app_version}" \ --output "$destination" \ "$url" } verify_release_assets() { local downloaded_binary="${temporary_directory}/${release_binary_name}" local downloaded_checksum="${temporary_directory}/${release_binary_name}.sha256" local downloaded_public_key="${temporary_directory}/server_public.pem" local published_checksum="" local actual_binary_checksum="" local actual_public_key_checksum="" log "Downloading signed release assets from GitHub" download_file "$release_binary_url" "$downloaded_binary" download_file "$release_checksum_url" "$downloaded_checksum" download_file "$release_public_key_url" "$downloaded_public_key" published_checksum="$( awk 'NF >= 1 {print $1; exit}' "$downloaded_checksum" )" [ "$published_checksum" = "$expected_binary_sha256" ] || fail "published binary checksum does not match this installer release" actual_binary_checksum="$( sha256sum "$downloaded_binary" | awk '{print $1}' )" [ "$actual_binary_checksum" = "$expected_binary_sha256" ] || fail "downloaded binary failed SHA-256 verification" actual_public_key_checksum="$( sha256sum "$downloaded_public_key" | awk '{print $1}' )" [ "$actual_public_key_checksum" = "$expected_public_key_sha256" ] || fail "downloaded license public key failed SHA-256 verification" chmod 0755 "$downloaded_binary" [ "$("$downloaded_binary" version)" = "${app_name} ${app_version}" ] || fail "downloaded binary version verification failed" log "Release assets verified" } record_existing_state() { if systemctl is-enabled "$verify_timer_name" >/dev/null 2>&1; then previous_timer_enabled=1 fi if systemctl is-active "$verify_timer_name" >/dev/null 2>&1; then previous_timer_active=1 fi } backup_existing_installation() { local timestamp="" timestamp="$(date -u +%Y%m%dT%H%M%SZ)" backup_directory="${backup_base}/${timestamp}" mkdir -p "$backup_directory" chmod 0700 "$backup_base" "$backup_directory" if [ -e "$install_root" ]; then cp -a "$install_root" "${backup_directory}/aitech-manager" fi if [ -e "$system_launcher" ] || [ -L "$system_launcher" ]; then cp -a "$system_launcher" "${backup_directory}/system-launcher" fi if [ -e "$verify_service" ]; then cp -a "$verify_service" "${backup_directory}/verify.service" fi if [ -e "$verify_timer" ]; then cp -a "$verify_timer" "${backup_directory}/verify.timer" fi } restore_previous_installation() { set +e trap - ERR printf '[AiTECH] Installation failed; restoring the previous state.\n' >&2 systemctl disable --now "$verify_timer_name" >/dev/null 2>&1 rm -rf "$install_root" rm -f "$system_launcher" "$verify_service" "$verify_timer" if [ -d "${backup_directory}/aitech-manager" ]; then cp -a "${backup_directory}/aitech-manager" "$install_root" fi if [ -e "${backup_directory}/system-launcher" ] || [ -L "${backup_directory}/system-launcher" ]; then cp -a "${backup_directory}/system-launcher" "$system_launcher" fi if [ -e "${backup_directory}/verify.service" ]; then cp -a "${backup_directory}/verify.service" "$verify_service" fi if [ -e "${backup_directory}/verify.timer" ]; then cp -a "${backup_directory}/verify.timer" "$verify_timer" fi systemctl daemon-reload >/dev/null 2>&1 if [ "$previous_timer_enabled" -eq 1 ]; then systemctl enable "$verify_timer_name" >/dev/null 2>&1 fi if [ "$previous_timer_active" -eq 1 ]; then systemctl start "$verify_timer_name" >/dev/null 2>&1 fi printf '[AiTECH] Previous installation restored.\n' >&2 set -e } cleanup() { local status=$? trap - EXIT if [ -n "$temporary_directory" ]; then rm -rf "$temporary_directory" fi if [ "$status" -ne 0 ] && [ "$installation_started" -eq 1 ] && [ "$installation_completed" -eq 0 ]; then restore_previous_installation fi exit "$status" } install_directories() { install -d -m 0755 "$install_root" "$build_dir" "$bin_dir" "$config_dir" "$log_dir" install -d -m 0700 "$temp_dir" install -d -m 0755 "$system_config_dir" "$system_log_dir" install -d -m 0700 "$system_certificate_dir" "$system_temp_dir" } install_runtime_files() { local downloaded_binary="${temporary_directory}/${release_binary_name}" local downloaded_public_key="${temporary_directory}/server_public.pem" install -m 0755 "$downloaded_binary" "$core_binary" install -m 0644 "$downloaded_public_key" "$server_public_key" cat > "$manager_config" < "$launcher" <<'LAUNCHER' #!/usr/bin/env bash set -Eeuo pipefail readonly core_binary="/opt/aitech-manager/build/aitech-core" if [ ! -x "$core_binary" ]; then printf 'AiTECH Manager core is missing or not executable: %s\n' \ "$core_binary" >&2 exit 1 fi exec "$core_binary" "$@" LAUNCHER chmod 0755 "$launcher" ln -sfn "$launcher" "$system_launcher" } install_verification_timer() { cat > "$verify_service" <<'SERVICE' [Unit] Description=AiTECH Manager License Verification After=network-online.target Wants=network-online.target ConditionPathExists=/opt/aitech-manager/build/aitech-core [Service] Type=oneshot User=root Group=root WorkingDirectory=/opt/aitech-manager ExecStart=/opt/aitech-manager/build/aitech-core verify TimeoutStartSec=30s UMask=0077 NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ReadWritePaths=/opt/aitech-manager/config ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictRealtime=true LockPersonality=true MemoryDenyWriteExecute=true SystemCallArchitectures=native RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 StandardOutput=journal StandardError=journal SERVICE cat > "$verify_timer" <<'TIMER' [Unit] Description=Schedule AiTECH Manager License Verification [Timer] OnBootSec=5min OnUnitActiveSec=6h RandomizedDelaySec=5min AccuracySec=1min Persistent=true Unit=aitech-manager-verify.service [Install] WantedBy=timers.target TIMER chmod 0644 "$verify_service" "$verify_timer" systemd-analyze verify "$verify_service" "$verify_timer" systemctl daemon-reload systemctl enable --now "$verify_timer_name" systemctl is-active --quiet "$verify_timer_name" systemctl is-enabled --quiet "$verify_timer_name" } activate_license() { log "Activating the client license" env -u LD_PRELOAD -u LD_AUDIT "$core_binary" activate "$license_key" env -u LD_PRELOAD -u LD_AUDIT "$core_binary" verify } verify_installed_files() { [ "$(sha256sum "$core_binary" | awk '{print $1}')" = "$expected_binary_sha256" ] || fail "installed binary checksum verification failed" [ "$(sha256sum "$server_public_key" | awk '{print $1}')" = "$expected_public_key_sha256" ] || fail "installed public key checksum verification failed" [ "$("$system_launcher" version)" = "${app_name} ${app_version}" ] || fail "installed launcher version verification failed" } main() { parse_arguments "$@" require_root detect_platform ensure_dependencies create_temporary_directory verify_release_assets if [ "$check_only" -eq 1 ]; then log "Installer readiness check passed" printf 'Release: %s\n' "$app_version" printf 'Binary SHA-256: %s\n' "$expected_binary_sha256" return 0 fi record_existing_state backup_existing_installation installation_started=1 log "Installing ${app_name} ${app_version}" install_directories install_runtime_files install_verification_timer verify_installed_files activate_license verify_installed_files installation_completed=1 log "Installation completed successfully" printf '\n' printf 'Run AiTECH Manager with:\n' printf ' sudo aitech\n' printf '\n' printf 'Version:\n' printf ' %s\n' "$("$system_launcher" version)" if find "$backup_directory" -mindepth 1 -print -quit | grep -q .; then printf '\nPrevious installation backup:\n' printf ' %s\n' "$backup_directory" else rmdir "$backup_directory" fi } trap cleanup EXIT main "$@"