#!/bin/bash

# Copyright 2025 Cybertrust Japan Co., Ltd.
#
# almalinux96als_install.sh
#   - Install script of the Cybertrust Japan's
#     AlmaLinux Advanced Longterm Support for 9.6.

readonly SCRIPT_VERSION="1.20 (2026/01/16)"

#
# EULA - End User Agreement of this script.
#
# "We," "us," "our" and "CTJ" refers to Cybertrust Japan Co., Ltd.
# "You" and "your" refers to the individual or entity that has
# acquired the this scripts. The parties hereto submits to the
# exclusive jurisdiction of the Tokyo District Court of Japan in
# respect to all controversies arising from or in relation to this
# Agreement, its performance, or any breach thereof.
# This Agreement shall be governed by and construed in accordance
# with the laws of Japan without reference to principles of conflict
# of laws.
#
# Section 1 - General
# This license agreement does not apply if there is a separate
# agreement between CTJ and the customer regarding the licensing of
# this product. The Japanese language version of this agreement will
# prevail in all respects over any translation, and all other versions
# are for convenience only and are not binding.
# This agreement is made in Japanese and translated into English.
# The Japanese text is the original and the English text is for
# reference purposes. If there is any conflict or inconsistency between
# these two texts, the Japanese text shall prevail.
#
# Section 2 - Export Controls and Sanctions Laws.
# Each Party shall comply with all applicable laws, regulations, rules,
# and governmental orders relating to export or re-export control or
# economic or trade sanctions of any country (collectively “Export
# Controls and Sanctions Laws”) in performing any of its obligations
# under this agreement.
#
# Section 3 - Disclaimer of Warranty.
# THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
# APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
# HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT
# WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
# OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
# ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
#
# Section 4 - Limitation of Liability.
# IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
# WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
# CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
# INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
# NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
# LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
# TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
# PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

# Define constants
readonly SCRIPT_NAME=$(basename "${0}")
readonly EXEC_DATE=$(date "+%Y%m%d")
readonly EXEC_TIME=$(date "+%H:%M:%S")
readonly LOGFILE="almalinuxals-${EXEC_DATE}-${EXEC_TIME//:/}.log"
readonly SUPPORTED_OS_VERSION="9.6"
readonly SUPPORTED_OS_REGEX="AlmaLinux release ${SUPPORTED_OS_VERSION}"
readonly SERVICE_NAME="AlmaLinux Advanced Longterm Support"
readonly SERVICE_FULLNAME="${SERVICE_NAME} for ${SUPPORTED_OS_VERSION}"
readonly DIST_RELEASE_PATH="/etc/almalinux-release"
readonly REPOFILEDIR_PATH="/etc/yum.repos.d"
readonly RPMGPGDIR_PATH="/etc/pki/rpm-gpg"
readonly REPOHOST_URL="https://certrepo.dist.miraclelinux.net"
readonly CERT_VALIDATION_URL="${REPOHOST_URL}/repo/almalinuxals/${SUPPORTED_OS_VERSION}/"
readonly RPMGPGDIR_URL="${REPOHOST_URL}/public"
readonly RPMGPG_KEYS=("RPM-GPG-KEY-TuxCare" "RPM-GPG-KEY-miraclelinux-v3")
readonly PEMFILE_NAME="ctj-almalinuxals.pem"
readonly PEMFILE_PATH="${REPOFILEDIR_PATH}/${PEMFILE_NAME}"
readonly REPOFILE_PATH="${REPOFILEDIR_PATH}/ctj-almalinuxals.repo"
readonly RELEASEPKG_NAME="ctj-almalinuxals-release"
readonly RELEASEPKG_URL="${REPOHOST_URL}/repo/almalinuxals/${SUPPORTED_OS_VERSION}/updates/x86_64/Packages/${RELEASEPKG_NAME}-${SUPPORTED_OS_VERSION}-1.el9.noarch.rpm"

readonly REPOFILE_CONTENT="# Cybertrust Japan ${SERVICE_NAME} repository

[CTJ-AlmaLinuxALS-1]
name=Cybertrust Japan ${SERVICE_NAME} for \$releasever - 1
baseurl=${REPOHOST_URL}/repo/almalinuxals/\$releasever/esu/\$basearch/
gpgcheck=1
enabled=1
gpgkey=file://${RPMGPGDIR_PATH}/${RPMGPG_KEYS[0]}
sslclientcert=${PEMFILE_PATH}
priority=1

[CTJ-AlmaLinuxALS-2]
name=Cybertrust Japan ${SERVICE_NAME} for \$releasever - 2
baseurl=${REPOHOST_URL}/repo/almalinuxals/\$releasever/updates/\$basearch/
gpgcheck=1
enabled=1
gpgkey=file://${RPMGPGDIR_PATH}/${RPMGPG_KEYS[1]}
sslclientcert=${PEMFILE_PATH}
priority=1"

# Define global variables
EXEC_MODE="NONE"
FORCE_MODE="FALSE"
X509FILE=""
X509PIN=""
TEMPDIR=""
LOG_CREATED=""

# Define errors
readonly ERROR_NONROOT_USER=10
readonly ERROR_EMPTY_PIN=11
readonly ERROR_INVALID_DIST=15
readonly ERROR_INVALID_VERSION=16
readonly ERROR_INVALID_ARCH=17
readonly ERROR_CONVERT_CERT=20
readonly ERROR_UNVERIFIED_CERT=21
readonly ERROR_GET_KEY=30
readonly ERROR_IMPORT_KEY=31
readonly ERROR_INSTALL_RELEASEPKG=50
readonly ERROR_NOT_INSTALLED=51
readonly ERROR_UNINSTALL_RELEASEPKG=52
readonly ERROR_NO_DEFAULTREPO_FOUND=90

# Define functions

echo_version() {
    echo "${SCRIPT_NAME} - Version ${SCRIPT_VERSION}"
    echo "  Install script of the Cybertrust Japan's ${SERVICE_FULLNAME}."
    echo "Copyright(C) 2025- Cybertrust Japan Co., Ltd."
    echo ""
}

check_root_user() {
    if [ "$(id -u)" -ne 0 ]; then
        echo "[Error] Please execute as root user." >&2
        exit ${ERROR_NONROOT_USER}
    fi
}

parse_args() {
    OPTERR=0
    while getopts ifrdvh-: OPT; do
        if [ "$OPT" = "-" ]; then
            OPT="-${OPTARG}"
        fi

        case "-$OPT" in
        -i | --install)
            if [ ${EXEC_MODE} = "NONE" ]; then
                EXEC_MODE="INSTALL"
            else
                EXEC_MODE="INVALID_COMBINATION"
            fi
            ;;
        -f | --force)
            FORCE_MODE="TRUE"
            ;;
        -r | --renew)
            if [ ${EXEC_MODE} = "NONE" ]; then
                EXEC_MODE="RENEW"
            else
                EXEC_MODE="INVALID_COMBINATION"
            fi
            ;;
        -d | --delete)
            if [ ${EXEC_MODE} = "NONE" ]; then
                EXEC_MODE="UNINSTALL"
            else
                EXEC_MODE="INVALID_COMBINATION"
            fi
            ;;
        -v | --version)
            if [ ${EXEC_MODE} = "NONE" ]; then
                EXEC_MODE="VERSION"
            else
                EXEC_MODE="INVALID_COMBINATION"
            fi
            ;;
        -h | --help)
            if [ ${EXEC_MODE} = "NONE" ]; then
                EXEC_MODE="HELP"
            else
                EXEC_MODE="INVALID_COMBINATION"
            fi
            ;;
        *)
            EXEC_MODE="INVALID_OPTION"
            ;;
        esac
    done

    if [ ${EXEC_MODE} = "INSTALL" ] || [ ${EXEC_MODE} = "RENEW" ]; then
        shift $((OPTIND - 1))
        X509FILE=${1}
        X509PIN=${2}
    fi
}

read_certificate_pin() {
    read -srp "Input X.509 client certification PIN: " X509PIN
    echo -e "\n"
    if [ -z "${X509PIN}" ]; then
        return ${ERROR_EMPTY_PIN}
    fi
}

prepare_certificate_info() {
    if [ -z "${X509FILE}" ]; then
        echo "[Error] No certificate file name specified." >&2
        EXEC_MODE="HELP"
        return
    fi

    if [ ! -e "${X509FILE}" ]; then
        echo "[Error] The specified certificate file doesn't exist." >&2
        EXEC_MODE="HELP"
        return
    fi

    if [ -z "${X509PIN}" ]; then
        if ! read_certificate_pin; then
            echo "[Error] The certificate PIN is empty." >&2
            EXEC_MODE="HELP"
            return
        fi
    fi
}

prepare_context() {
    parse_args "$@"

    case ${EXEC_MODE} in
    INSTALL | RENEW)
        prepare_certificate_info
        ;;
    INVALID_COMBINATION)
        echo "[Error] Invalid option combination." >&2
        EXEC_MODE="HELP"
        ;;
    INVALID_OPTION)
        echo "[Error] Invalid option string." >&2
        EXEC_MODE="HELP"
        ;;
    esac
}

put_log() {
    local -r message=${1}

    if [ -z "${LOG_CREATED}" ]; then
        # Initial processing
        LOG_CREATED="TRUE"
        echo_version >>"${LOGFILE}"
        echo "Exec. Date/Time: ${EXEC_DATE} ${EXEC_TIME}" >>"${LOGFILE}"
    fi

    if [ "${FUNCNAME[1]}" = "put_err" ]; then
        echo "${FUNCNAME[2]}(): ${message}" >>"${LOGFILE}"
    else
        echo "${FUNCNAME[1]}(): ${message}" >>"${LOGFILE}"
    fi
}

put_err() {
    local -r message=${1}

    put_log "${message}"
    echo "${message}" >&2
}

check_environment() {
    if [ ! -e ${DIST_RELEASE_PATH} ]; then
        put_err "[Error] This distribution is not supported."
        return ${ERROR_INVALID_DIST}
    fi

    if ! grep -q "${SUPPORTED_OS_REGEX}" ${DIST_RELEASE_PATH}; then
        put_err "[Error] This OS version is not supported."
        return ${ERROR_INVALID_VERSION}
    fi

    if [ "$(uname -m)" != "x86_64" ]; then
        put_err "[Error] This architecture is not supported."
        return ${ERROR_INVALID_ARCH}
    fi

    put_log "[Info] The environment check passed."
}

convert_pkcs12_to_pem() {
    local -r src_pkcs12_path=${1}
    local -r pinfile_path=${2}
    local -r dest_pem_path=${3}

    if ! openssl pkcs12 -passin file:"${pinfile_path}" -in "${src_pkcs12_path}" \
        -out "${dest_pem_path}" -legacy -clcerts -nodes >/dev/null 2>&1; then
        put_err "[Error] Failed to convert certificate format."
        return ${ERROR_CONVERT_CERT}
    fi

    put_log "[Info] Certificate conversion complete."
}

check_certificate_validity() {
    local -r pemfile_path=${1}

    HTTP_STATUS_CODE=$(curl -sL ${CERT_VALIDATION_URL} -E "${pemfile_path}" \
        -o /dev/null -w '%{http_code}')
    if [ "${HTTP_STATUS_CODE}" -ne 200 ]; then
        put_err "[Error] Failed to validate certificate validity."
        return ${ERROR_UNVERIFIED_CERT}
    fi

    put_log "[Info] The certificate validity check passed."
}

change_repository_to_ctj_mirror() {
    local reponame
    for reponame in "BaseOS" "AppStream" "extras"; do
        local repoid=$(echo ${reponame} | tr '[:upper:]' '[:lower:]')
        local repofile="${REPOFILEDIR_PATH}/almalinux-${repoid}.repo"

        if [ ! -e "${repofile}" ]; then
            put_err "[Error] ${repofile} does not exist."
            return ${ERROR_NO_DEFAULTREPO_FOUND}
        fi

        # If base url has not been modified, modify it.
        if ! grep -q "^baseurl=${REPOHOST_URL}/repo/almalinux/\$releasever/${reponame}/\$basearch/os/" "${repofile}"; then
            cp -pf "${repofile}" "${TEMPDIR}" # for revert process

            # Comment out all mirrorlist and baseurl entries within the target section.
            sed -i -e "/^\[${repoid}\]/,/^\[/ {
                /^\s*mirrorlist=/s/^/# /;
                /^\s*baseurl=/s/^/# /;
            }" "${repofile}"

            # Add settings to access the CTJ mirror repository.
            sed -i "/^\[${repoid}\]/a sslclientcert=${PEMFILE_PATH}" "${repofile}"
            sed -i "/^\[${repoid}\]/a baseurl=${REPOHOST_URL}/repo/almalinux/\$releasever/${reponame}/\$basearch/os/" "${repofile}"

            put_log "[Info] ${repofile} modification completed."
        else
            put_log "[Info] ${repofile} has already been modified."
        fi
    done
}

revert_repofiles() {
    local reponame
    for reponame in "BaseOS" "AppStream" "extras"; do
        local repoid=$(echo ${reponame} | tr '[:upper:]' '[:lower:]')
        local repofilename="almalinux-${repoid}.repo"

        if [ ! -e "${TEMPDIR}/${repofilename}" ]; then
            put_log "[Info] The reversion process has been skipped (no change): ${repofilename}"
            continue
        fi

        cp -pf "${TEMPDIR}/${repofilename}" "${REPOFILEDIR_PATH}/${repofilename}"
        put_log "[Info] The repofile has been reverted: ${repofilename}"
    done
}

set_releasever() {
    if [ -e /etc/dnf/vars/releasever ]; then
        cp -pf /etc/dnf/vars/releasever "${TEMPDIR}" # for revert process
        if [ "$(cat /etc/dnf/vars/releasever)" != ${SUPPORTED_OS_VERSION} ]; then
            cp -f /etc/dnf/vars/releasever "/etc/dnf/vars/releasever.ctj${EXEC_DATE}bak"
            put_log "[Info] A backup of the releasever file has been created."

            # for revert process
            cp -f /etc/dnf/vars/releasever "${TEMPDIR}/releasever.ctj${EXEC_DATE}bak"
        fi
    fi

    echo "${SUPPORTED_OS_VERSION}" >/etc/dnf/vars/releasever
    touch "${TEMPDIR}/is_set_releasever" # for revert process
    put_log "[Info] The releasever file configuration is complete."
}

revert_releasever() {
    if [ ! -e "${TEMPDIR}/is_set_releasever" ]; then
        put_log "[Info] Since set_releasever() has not been executed, do nothing."
        return
    fi

    if [ ! -e "${TEMPDIR}/releasever" ]; then
        # releasever did not exist prior to this installation process.
        rm -f /etc/dnf/vars/releasever
        put_log "[Info] releasever has been removed."
        return
    fi

    cp -pf "${TEMPDIR}/releasever" /etc/dnf/vars/releasever
    put_log "[Info] releasever has been reverted."

    if [ -e "${TEMPDIR}/releasever.ctj${EXEC_DATE}bak" ]; then
        # If a backup file was created this time, delete it.
        rm -f "/etc/dnf/vars/releasever.ctj${EXEC_DATE}bak"
        put_log "[Info] The backup file has been deleted."
    fi
}

apply_service_specific_settings() {
    change_repository_to_ctj_mirror || return $?
    set_releasever
}

revert_service_specific_settings() {
    revert_releasever
    revert_repofiles
}

fetch_and_import_gpgkey() {
    local -r gpgkey_filename=${1}

    if ! curl -sL "${RPMGPGDIR_URL}/${gpgkey_filename}" \
        -o "${TEMPDIR}/${gpgkey_filename}"; then
        put_err "[Error] Failed to fetch GPG key: ${gpgkey_filename}."
        return ${ERROR_GET_KEY}
    fi

    if ! rpm --import "${TEMPDIR}/${gpgkey_filename}"; then
        put_err "[Error] Failed to import GPG key: ${gpgkey_filename}."
        return ${ERROR_IMPORT_KEY}
    fi
    cp -f "${TEMPDIR}/${gpgkey_filename}" ${RPMGPGDIR_PATH}

    put_log "[Info] The GPG key has been imported: ${gpgkey_filename}."
}

remove_gpgkey() {
    local -r gpgkey_filename=${1}

    rm -f "${RPMGPGDIR_PATH}/${gpgkey_filename}"
    put_log "[Info] The GPG key has been deleted: ${gpgkey_filename}."
}

set_certificate_file() {
    local -r src_pem_path=${1}
    cp -f "${src_pem_path}" ${PEMFILE_PATH}
    chmod +r ${PEMFILE_PATH} # for non-root user
    put_log "[Info] The certificate file has been placed."
}

remove_certificate_file() {
    rm -f ${PEMFILE_PATH}
    put_log "[Info] The certificate file has been removed."
}

set_repository_file() {
    echo "${REPOFILE_CONTENT}" >${REPOFILE_PATH}
    put_log "[Info] The repofile has been created."
}

remove_repository_file() {
    rm -f ${REPOFILE_PATH}
    put_log "[Info] The repofile has been removed."
}

install_release_package() {
    dnf clean all >/dev/null
    if ! dnf install -y --setopt=sslclientcert=${PEMFILE_PATH} ${RELEASEPKG_URL} \
        >/dev/null; then
        put_err "[Error] Failed to install release package."
        return ${ERROR_INSTALL_RELEASEPKG}
    fi
    put_log "[Info] The release package has been installed."
}

uninstall_release_package() {
    if ! dnf remove -y ${RELEASEPKG_NAME} >/dev/null; then
        put_err "[Error] Failed to uninstall release package."
        return ${ERROR_UNINSTALL_RELEASEPKG}
    fi
    put_log "[Info] The release package has been uninstalled."
}

revert_installation() {
    remove_repository_file
    remove_certificate_file

    local gpgkey
    for gpgkey in "${RPMGPG_KEYS[@]}"; do
        remove_gpgkey "${gpgkey}"
    done

    revert_service_specific_settings
}

install() {
    put_log "[Info] Run in install mode."

    if [ ${FORCE_MODE} = "FALSE" ]; then
        check_environment || exit $?
    else
        put_log "[Warn] Environment checks were skipped because force mode is enabled."
    fi

    TEMPDIR=$(mktemp -d)
    trap 'rm -rf "${TEMPDIR}"' EXIT

    local -r pinfile=$(mktemp -p "${TEMPDIR}")
    echo "${X509PIN}" >"${pinfile}"
    convert_pkcs12_to_pem \
        "${X509FILE}" "${pinfile}" "${TEMPDIR}/${PEMFILE_NAME}" || exit $?

    check_certificate_validity "${TEMPDIR}/${PEMFILE_NAME}" || exit $?

    trap 'RC=$?; revert_installation; exit $RC' ERR

    apply_service_specific_settings

    local gpgkey
    for gpgkey in "${RPMGPG_KEYS[@]}"; do
        fetch_and_import_gpgkey "${gpgkey}"
    done

    set_certificate_file "${TEMPDIR}/${PEMFILE_NAME}"
    set_repository_file
    install_release_package

    echo "Successfully completed CTJ's ${SERVICE_NAME} repository settings!"
    echo "You can now update your system using the \"dnf update\" command."
    put_log "[Info] Installation completed."
}

check_release_package_installed() {
    if ! rpm -q ${RELEASEPKG_NAME}-${SUPPORTED_OS_VERSION} >/dev/null; then
        echo "[Error] The target service is not installed." >&2
        exit ${ERROR_NOT_INSTALLED}
    fi
}

uninstall() {
    check_release_package_installed

    put_log "[Info] Run in uninstall mode."

    uninstall_release_package || exit $?
    remove_repository_file
    remove_certificate_file

    local gpgkey
    for gpgkey in "${RPMGPG_KEYS[@]}"; do
        remove_gpgkey "${gpgkey}"
    done

    echo "Successful uninstallation of CTJ's ${SERVICE_NAME}."
    echo "Thank you very much for using our support service."
    put_log "[Info] Uninstallation completed."
}

renew() {
    check_release_package_installed

    put_log "[Info] Run in renew mode."

    TEMPDIR=$(mktemp -d)
    trap 'rm -rf "${TEMPDIR}"' EXIT

    local -r pinfile=$(mktemp -p "${TEMPDIR}")
    echo "${X509PIN}" >"${pinfile}"
    convert_pkcs12_to_pem \
        "${X509FILE}" "${pinfile}" "${TEMPDIR}/${PEMFILE_NAME}" || exit $?

    check_certificate_validity "${TEMPDIR}/${PEMFILE_NAME}" || exit $?
    set_certificate_file "${TEMPDIR}/${PEMFILE_NAME}"

    echo "Successfully completed client certificate renewal!"
    echo "You can now update your system using the \"dnf update\" command."
    put_log "[Info] Certificate renewal completed."
}

echo_usage() {
    echo "Usage: ${SCRIPT_NAME} [OPTION]..."
    echo "  -i, --install [-f, --force] <X.509 cert. file (.p12)> [X.509 cert. PIN]"
    echo "                  : Install the CTJ's ${SERVICE_NAME}"
    echo "                      * If you omit the PIN, you will be required to enter it"
    echo "                        during script execution."
    echo "  -r, --renew <X.509 cert. file (.p12)> [X.509 cert. PIN]"
    echo "                  : Renew the client certificate for the CTJ's ${SERVICE_NAME}"
    echo "                      * If you omit the PIN, you will be required to enter it"
    echo "                        during script execution."
    echo "  -d, --delete    : Delete CTJ's ${SERVICE_NAME} settings"
    echo "  -h, --help      : Show this messages and exit"
    echo "  -v, --version   : Show this script's version"
    echo ""
}

main() {
    echo_version
    check_root_user

    prepare_context "$@"
    case ${EXEC_MODE} in
    INSTALL)
        install
        ;;
    UNINSTALL)
        uninstall
        ;;
    RENEW)
        renew
        ;;
    HELP | NONE)
        echo_usage
        ;;
    VERSION)
        # do nothing
        ;;
    esac
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi
