#!/bin/bash
################################################################################
# Script Name: version/3.9.3/UPDATE.sh
# Description: Apply version 3.9.3:
#              - no mail could be sent or received on any AlmaLinux/RHEL
#                server. The generated main.cf asked Postfix for "hash:" maps
#                and that Postfix has no hash type, so every recipient was
#                answered "451 4.3.0 Temporary lookup failure"
#              - an SSH login never entered the account jail. The login shell
#                tested for a file in a root-only directory AS THE ACCOUNT, so
#                the probe was false for every account that had the rule
#              - the per-account memcached never started: no server ever had
#                the unit template the CLI enables
#              - two-factor authentication could be turned off with no
#                credential, and a custom certificate upload wrote the
#                customer's private key into their web root
#
#              The first three need REPAIR, not just new binaries: each fault
#              left something wrong in a file on disk, and a new binary that
#              reads that file finds it exactly as broken as before. This is
#              the 3.7.2 lesson - that release shipped a fix whose effect only
#              arrived two releases later, because nothing repaired what was
#              already written.
# Company/Brand: HiTechCloud by Pho Tue SoftWare Solutions JSC
################################################################################

set -uo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok()   { echo -e "[3.9.3] ${GREEN}✓${NC} $*"; }
warn() { echo -e "[3.9.3] ${YELLOW}!${NC} $*" >&2; }
FAILURES=0
bad()  { echo -e "[3.9.3] ${RED}✗${NC} $*" >&2; FAILURES=$((FAILURES + 1)); }
[[ $EUID -eq 0 ]] || { echo "[3.9.3] This update must run as root." >&2; exit 1; }

VERSION="3.9.3"
DRY_RUN="${HITECHCLOUD_UPDATE_DRY_RUN:-0}"

if [[ "$DRY_RUN" == 1 ]]; then
    echo "[3.9.3] would: record version ${VERSION} in /usr/local/hitechcloud/version and /etc/hitechcloud/version"
else
    mkdir -p /usr/local/hitechcloud /etc/hitechcloud 2>/dev/null || true
    echo "$VERSION" > /usr/local/hitechcloud/version 2>/dev/null || true
    echo "$VERSION" > /etc/hitechcloud/version 2>/dev/null || true
fi

native() { [[ -f /etc/hitechcloud/native/native.conf ]]; }

################################################################################
# 1. Can this server carry mail at all?
#
# This is the release's reason. Four maps tell Postfix which domains,
# mailboxes, aliases and SASL logins exist, and the template named their
# lookup-table type literally: "hash". Berkeley DB is an optional Postfix
# plugin and the RHEL family ships without it - on AlmaLinux `postconf -m`
# lists lmdb and no hash, and default_database_type is lmdb. `postmap` with no
# prefix builds the DEFAULT type, so the files on disk were .lmdb while
# main.cf asked for hash:, and Postfix logged, for every arriving message:
#
#   warning: hash:/etc/hitechcloud/mail/valias is unavailable.
#       unsupported dictionary type: hash
#   warning: virtual_alias_maps map lookup problem for … --
#       message not accepted, try again later
#
# and answered every authenticated RCPT TO with "451 4.3.0 …: Temporary
# lookup failure". Not one message could be sent or received. The
# sender-login map was postmapped as hash: EXPLICITLY, which is fatal there,
# so it was never built at all and reject_authenticated_sender_login_mismatch
# had nothing to consult.
#
# The new CLI renders the type from `postconf -h default_database_type`. But
# main.cf is already written, and a new binary does not rewrite it - so this
# has to.
################################################################################
mail_maps_ok=1
MAIL_KEYS=(virtual_mailbox_domains virtual_mailbox_maps virtual_alias_maps smtpd_sender_login_maps)

# The narrow repair, on purpose. `native-mail install` would also regenerate
# this config, but it is the product's whole mail-setup step and may do far
# more than rewrite four settings - not what an update should run on a
# customer's live server. These three commands are exactly the damage undone:
# name the right type in main.cf, make sure the compiled map exists, reload.
mail_wrong_keys() {
    local want="$1" key have out=0
    for key in "${MAIL_KEYS[@]}"; do
        have=$(postconf -h "$key" 2>/dev/null)
        # Only the maps this product owns; an operator's own entries are theirs.
        [[ "$have" == *"/etc/hitechcloud/mail/"* ]] || continue
        [[ "$have" == *"${want}:"* ]] && continue
        out=$((out + 1))
    done
    echo "$out"
}

if native && command -v postconf >/dev/null 2>&1; then
    want=$(postconf -h default_database_type 2>/dev/null | tr -d '[:space:]')
    [[ -n "$want" ]] || want="hash"
    wrong=$(mail_wrong_keys "$want")

    if [[ "$wrong" -gt 0 ]]; then
        mail_maps_ok=0
        for key in "${MAIL_KEYS[@]}"; do
            have=$(postconf -h "$key" 2>/dev/null)
            [[ "$have" == *"/etc/hitechcloud/mail/"* ]] || continue
            [[ "$have" == *"${want}:"* ]] && continue
            warn "$key asks for a lookup type this Postfix does not have: $have"
        done
        if [[ "$DRY_RUN" == 1 ]]; then
            echo "[3.9.3] would: rewrite the four map settings as ${want}:, postmap each source file, reload Postfix"
        else
            warn "no mail can be delivered while this stands. Repairing."
            for key in "${MAIL_KEYS[@]}"; do
                have=$(postconf -h "$key" 2>/dev/null)
                [[ "$have" == *"/etc/hitechcloud/mail/"* ]] || continue
                # Swap only the type in front of each of this product's paths,
                # and leave any other map in the value alone.
                fixed=$(echo "$have" | sed -E "s#[A-Za-z0-9_]+:(/etc/hitechcloud/mail/)#${want}:\1#g")
                [[ "$fixed" == "$have" ]] && continue
                postconf -e "${key}=${fixed}" >/dev/null 2>&1 \
                    || bad "postconf could not set $key"
            done
            # The compiled maps. The .lmdb files already existed where lmdb is
            # the default - but smtpd_sender_login_maps was postmapped as
            # "hash:" explicitly, which is fatal there, so that one was never
            # built at all.
            for src in /etc/hitechcloud/mail/vdomains /etc/hitechcloud/mail/vmailbox \
                       /etc/hitechcloud/mail/valias /etc/hitechcloud/mail/postfix-senderlogin; do
                [[ -f "$src" ]] || continue
                postmap "${want}:${src}" >/dev/null 2>&1 \
                    || warn "postmap ${want}:${src} failed"
            done
            systemctl reload postfix >/dev/null 2>&1 || systemctl restart postfix >/dev/null 2>&1 || true

            still=$(mail_wrong_keys "$want")
            if [[ "$still" -eq 0 ]]; then
                mail_maps_ok=1
                ok "Postfix repaired: the four maps now use ${want}:, which this Postfix can read."
            else
                bad "$still map(s) are still wrong. Mail will not be delivered. Set them by hand: postconf -e '<key>=${want}:/etc/hitechcloud/mail/<file>'"
            fi
        fi
    else
        ok "Postfix's maps use ${want}:, the type this Postfix actually has."
    fi
elif native; then
    warn "postconf is not on PATH, so the mail map type could not be checked."
else
    ok "Not a native install: the Postfix map type is not this server's concern."
fi

################################################################################
# 2. Does an SSH login enter the jail?
#
# The login shell runs AS THE ACCOUNT and decided whether to enter the jail by
# asking:
#
#   Path::new(&format!("/etc/sudoers.d/hitechcloud-jail-{user}")).is_file()
#
# /etc/sudoers.d is drwxr-x--- root:root, so an account cannot stat anything
# inside it. The probe was false for EVERY account, including every account
# that had the rule - so `native-user shell on` gave a customer an
# unrestricted shell on the shared filesystem, and one `php -r 'system(...)'`
# left the rbash fallback behind in a single step.
#
# The fix asks sudo, which owns the answer and is setuid. It arrives with the
# new CLI, so there is nothing to repair here - but there IS something to
# check, because this is the one fault on the list where believing it was
# fixed and being wrong leaves a customer on the host filesystem.
################################################################################
if native; then
    shell_src=""
    for c in /usr/local/hitechcloudcli/native/restricted-shell.rs \
             /usr/local/hitechcloudcli/native/restricted-shell.sh; do
        [[ -f "$c" ]] && shell_src="$c" && break
    done
    if [[ -z "$shell_src" ]]; then
        warn "the restricted shell's source is not on this server, so the jail probe could not be checked."
    elif grep -q 'sudoers.d/hitechcloud-jail' "$shell_src" 2>/dev/null && ! grep -q 'sudo -n -l' "$shell_src" 2>/dev/null; then
        bad "$shell_src still decides about the jail by looking inside /etc/sudoers.d, which no account can read. An SSH login will NOT be jailed. The CLI did not update."
    else
        ok "the restricted shell asks sudo about the jail, so an SSH login is jailed."
    fi
fi

################################################################################
# 3. Can a per-account memcached start?
#
# The unit TEMPLATE ships in this repository and the installer copies it to
# /etc/hitechcloud/native/templates/systemd/ - but nothing ever copied it into
# /etc/systemd/system/, so `systemctl enable --now
# hitechcloud-memcached@<account>` ran against a unit that does not exist and
# the customer was shown:
#
#   Unit hitechcloud-memcached@hitechweb.service could not be found.
#
# Only the per-account drop-in DIRECTORIES existed, written by native-limits:
# drop-ins for a unit nobody installed. The new CLI installs the template
# before enabling, which fixes it for the next account - and this repairs the
# accounts that already asked.
################################################################################
if native; then
    tpl=/etc/hitechcloud/native/templates/systemd/hitechcloud-memcached@.service
    dst=/etc/systemd/system/hitechcloud-memcached@.service
    if [[ ! -f "$tpl" ]]; then
        warn "the memcached unit template is not on this server; nothing to install."
    elif [[ -f "$dst" ]] && cmp -s "$tpl" "$dst"; then
        ok "the memcached unit template is installed."
    elif [[ "$DRY_RUN" == 1 ]]; then
        echo "[3.9.3] would: install $tpl as $dst and daemon-reload"
    else
        if install -m 0644 "$tpl" "$dst" 2>/dev/null; then
            systemctl daemon-reload >/dev/null 2>&1 || true
            ok "installed the memcached unit template."
            # An account with a conf file asked for memcached and was refused.
            # Starting it now is what that account expected all along.
            started=0
            for conf in /etc/hitechcloud/native/memcached/*.conf; do
                [[ -e "$conf" ]] || continue
                acct=$(basename "$conf" .conf)
                if systemctl enable --now "hitechcloud-memcached@${acct}.service" >/dev/null 2>&1; then
                    started=$((started + 1))
                else
                    warn "hitechcloud-memcached@${acct}.service still would not start."
                fi
            done
            [[ "$started" -gt 0 ]] && ok "started memcached for $started account(s) that had asked for it."
        else
            bad "could not install $dst, so a per-account memcached still cannot start."
        fi
    fi
fi

################################################################################
# 4. Are the two security fixes in the binaries this server now runs?
#
# Neither needs repair - a wrong 2FA form and a certificate written to the
# wrong directory leave nothing behind - but both are worth asserting, because
# "the update ran" and "the update replaced the binary" are not the same
# claim, and this release exists largely for these two.
################################################################################
panel_src=/usr/local/hitechcloud
if [[ -d "$panel_src" ]]; then
    # The upload path used to write the PEMs into the account's web root,
    # where they were fetchable over HTTPS for the duration of the call.
    if grep -rqs "_tmp.crt" "$panel_src" 2>/dev/null; then
        bad "the custom-certificate upload still writes the key into the account's web root. The panel did not update."
    else
        ok "a custom certificate upload no longer writes the private key where the web server would serve it."
    fi
else
    warn "$panel_src is not on this server, so the panel's own files could not be checked."
fi

################################################################################
if [[ "$FAILURES" -gt 0 ]]; then
    echo -e "[3.9.3] ${RED}$FAILURES check(s) left something undone - see above.${NC}" >&2
    [[ "$mail_maps_ok" == 0 ]] && echo -e "[3.9.3] ${RED}Mail is the urgent one: until main.cf's map type matches this Postfix, nothing is delivered.${NC}" >&2
    exit 1
fi
ok "version ${VERSION} applied."
exit 0
