#!/usr/bin/env bash set -euo pipefail if [[ $# -ne 2 ]]; then echo "Usage: $0 ARCHIVE SHA256_OR_SHA256_FILE" >&2 exit 2 fi if [[ ${EUID} -ne 0 ]]; then echo "Run this installer as root." >&2 exit 2 fi archive_path="$(readlink -f -- "$1")" checksum_input="$2" install_root="${NETFISHING_INSTALL_ROOT:-/opt/netfishing-server}" service_name="${NETFISHING_SERVICE_NAME:-netfishing-server.service}" if [[ ! -f "${archive_path}" ]]; then echo "Archive not found: $1" >&2 exit 1 fi if [[ "${install_root}" != /* || "${install_root}" == "/" ]]; then echo "NETFISHING_INSTALL_ROOT must be a specific absolute directory." >&2 exit 2 fi if [[ "${checksum_input}" =~ ^[0-9A-Fa-f]{64}$ ]]; then expected_sha256="${checksum_input,,}" elif [[ -f "${checksum_input}" ]]; then expected_sha256="$(awk 'NF {print tolower($1); exit}' "${checksum_input}")" else echo "Second argument must be a SHA-256 hash or checksum file." >&2 exit 2 fi if [[ ! "${expected_sha256}" =~ ^[0-9a-f]{64}$ ]]; then echo "Invalid expected SHA-256 value." >&2 exit 2 fi actual_sha256="$(sha256sum "${archive_path}" | awk '{print $1}')" if [[ "${actual_sha256}" != "${expected_sha256}" ]]; then echo "Archive SHA-256 mismatch." >&2 exit 1 fi work_dir="$(mktemp -d /tmp/netfishing-server-install.XXXXXX)" release_stage="" cleanup() { if [[ -n "${release_stage}" && -d "${release_stage}" ]]; then rm -rf -- "${release_stage}" fi rm -rf -- "${work_dir}" } trap cleanup EXIT top_directory="" while IFS= read -r member; do [[ -n "${member}" ]] || continue if [[ "${member}" == /* ]]; then echo "Archive contains an absolute path." >&2 exit 1 fi IFS='/' read -r -a path_parts <<< "${member}" for part in "${path_parts[@]}"; do if [[ "${part}" == ".." || "${part}" == "." ]]; then echo "Archive contains an unsafe path." >&2 exit 1 fi done member_top="${member%%/*}" if [[ -z "${top_directory}" ]]; then top_directory="${member_top}" elif [[ "${member_top}" != "${top_directory}" ]]; then echo "Archive must contain exactly one top-level directory." >&2 exit 1 fi done < <(tar -tzf "${archive_path}") if [[ -z "${top_directory}" ]]; then echo "Archive is empty." >&2 exit 1 fi tar --no-same-owner --no-same-permissions -xzf "${archive_path}" -C "${work_dir}" payload_dir="${work_dir}/${top_directory}" if [[ -n "$(find "${payload_dir}" -type l -print -quit)" ]]; then echo "Archive payload may not contain symbolic links." >&2 exit 1 fi for filename in NETfishingServer.x86_64 NETfishingServer.pck BUILD_INFO SHA256SUMS; do if [[ ! -f "${payload_dir}/${filename}" ]]; then echo "Archive is missing ${filename}." >&2 exit 1 fi done (cd -- "${payload_dir}" && sha256sum -c SHA256SUMS) manifest_value() { local key="$1" awk -F= -v key="${key}" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' \ "${payload_dir}/BUILD_INFO" } manifest_version="$(manifest_value manifest_version)" game_version="$(manifest_value game_version)" game_commit="$(manifest_value game_commit)" platform="$(manifest_value platform)" if [[ "${manifest_version}" != "1" ]]; then echo "Unsupported BUILD_INFO format." >&2 exit 1 fi if [[ ! "${game_version}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then echo "Invalid game version in BUILD_INFO." >&2 exit 1 fi if [[ ! "${game_commit}" =~ ^[0-9a-f]{40}$ ]]; then echo "Invalid game commit in BUILD_INFO." >&2 exit 1 fi if [[ "${platform}" != "linux-x86_64" ]]; then echo "This installer only accepts linux-x86_64 packages." >&2 exit 1 fi releases_dir="${install_root}/releases" release_dir="${releases_dir}/${game_version}-${game_commit:0:12}" install -d -m 0755 "${install_root}" "${releases_dir}" if [[ -e "${release_dir}" ]]; then if [[ ! -f "${release_dir}/BUILD_INFO" ]] \ || ! cmp -s "${payload_dir}/BUILD_INFO" "${release_dir}/BUILD_INFO"; then echo "A different release already occupies ${release_dir}." >&2 exit 1 fi (cd -- "${release_dir}" && sha256sum -c SHA256SUMS) else release_stage="$(mktemp -d "${releases_dir}/.incoming.XXXXXX")" install -m 0555 "${payload_dir}/NETfishingServer.x86_64" "${release_stage}/" install -m 0444 "${payload_dir}/NETfishingServer.pck" "${release_stage}/" install -m 0444 "${payload_dir}/BUILD_INFO" "${release_stage}/" install -m 0444 "${payload_dir}/SHA256SUMS" "${release_stage}/" if [[ -f "${payload_dir}/README.md" ]]; then install -m 0444 "${payload_dir}/README.md" "${release_stage}/" fi chmod 0755 "${release_stage}" mv -- "${release_stage}" "${release_dir}" release_stage="" fi replace_symlink() { local target="$1" local link_path="$2" local temporary_link="${install_root}/.$(basename -- "${link_path}").new.$$" ln -s -- "${target}" "${temporary_link}" mv -Tf -- "${temporary_link}" "${link_path}" } old_current="" if [[ -L "${install_root}/current" ]]; then old_current="$(readlink -f -- "${install_root}/current")" fi service_exists=false service_active=false if command -v systemctl >/dev/null && systemctl cat "${service_name}" >/dev/null 2>&1; then service_exists=true if systemctl is-active --quiet "${service_name}"; then service_active=true unit_text="$(systemctl cat "${service_name}")" if [[ "${unit_text}" != *"${install_root}/current/NETfishingServer.x86_64"* ]]; then echo "Active ${service_name} does not use the current-release symlink." >&2 echo "Migrate the unit while stopped before using this updater." >&2 exit 1 fi fi fi if ${service_active}; then systemctl stop "${service_name}" fi replace_symlink "${release_dir}" "${install_root}/current" if ${service_active}; then if ! systemctl start "${service_name}"; then start_succeeded=false else sleep 1 start_succeeded=true systemctl is-active --quiet "${service_name}" || start_succeeded=false fi if ! ${start_succeeded}; then echo "New release failed to start; restoring the previous release." >&2 if [[ -n "${old_current}" ]]; then replace_symlink "${old_current}" "${install_root}/current" else unlink -- "${install_root}/current" fi systemctl start "${service_name}" || true exit 1 fi fi if [[ -n "${old_current}" && "${old_current}" != "${release_dir}" ]]; then replace_symlink "${old_current}" "${install_root}/previous" fi echo "Installed NETfishing dedicated server ${game_version} (${game_commit})." if ${service_exists} && ! ${service_active}; then echo "${service_name} was inactive and was not started." fi